// 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.IO;
using System.Reflection;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests.BackEnd;
using Xunit;

#nullable disable

namespace Microsoft.Build.UnitTests.Construction
{
    /// <summary>
    /// Tests for the ElementLocation class
    /// </summary>
    public class ElementLocation_Tests
    {
        /// <summary>
        /// Path to the common targets
        /// </summary>
        private string _pathToCommonTargets =
#if FEATURE_INSTALLED_MSBUILD
            Path.Combine(FrameworkLocationHelper.PathToDotNetFrameworkV45, "Microsoft.Common.targets");
#else
            Path.Combine(AppContext.BaseDirectory, "Microsoft.Common.targets");
#endif

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest1()
        {
            IElementLocation location = ElementLocation.Create("file", 65536, 0);
            Assert.Equal("file", location.File);
            Assert.Equal(65536, location.Line);
            Assert.Equal(0, location.Column);
            Assert.Contains("RegularElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest2()
        {
            IElementLocation location = ElementLocation.Create("file", 0, 65536);
            Assert.Equal("file", location.File);
            Assert.Equal(0, location.Line);
            Assert.Equal(65536, location.Column);
            Assert.Contains("RegularElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest3()
        {
            IElementLocation location = ElementLocation.Create("file", 65536, 65537);
            Assert.Equal("file", location.File);
            Assert.Equal(65536, location.Line);
            Assert.Equal(65537, location.Column);
            Assert.Contains("RegularElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Test equality
        /// </summary>
        [Fact]
        public void Equality()
        {
            IElementLocation location1 = ElementLocation.Create("file", 65536, 65537);
            IElementLocation location2 = ElementLocation.Create("file", 0, 1);
            IElementLocation location3 = ElementLocation.Create("file", 0, 65537);
            IElementLocation location4 = ElementLocation.Create("file", 65536, 1);
            IElementLocation location5 = ElementLocation.Create("file", 0, 1);
            IElementLocation location6 = ElementLocation.Create("file", 65536, 65537);

            Assert.True(location1.Equals(location6));
            Assert.True(location2.Equals(location5));
            Assert.False(location3.Equals(location1));
            Assert.False(location4.Equals(location2));
            Assert.False(location4.Equals(location6));
        }

        /// <summary>
        /// Check it will use large element location when it should.
        /// Using file as BIZARRELY XmlTextReader+StringReader crops or trims.
        /// </summary>
        [Fact]
        public void TestLargeElementLocationUsedLargeColumn()
        {
            string file = null;

            try
            {
                file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

                File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents("<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>\r\n<ItemGroup>") + new string(' ', 70000) + @"<x/></ItemGroup></Project>");

                ProjectRootElement.Open(file);
            }
            catch (InvalidProjectFileException ex)
            {
                Assert.Equal(70012, ex.ColumnNumber);
                Assert.Equal(2, ex.LineNumber);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Check it will use large element location when it should.
        /// Using file as BIZARRELY XmlTextReader+StringReader crops or trims.
        /// </summary>
        [Fact]
        public void TestLargeElementLocationUsedLargeLine()
        {
            string file = null;

            try
            {
                string longstring = String.Empty;

                for (int i = 0; i < 7000; i++)
                {
                    longstring += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n";
                }

                file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

                File.WriteAllText(file, ObjectModelHelpers.CleanupFileContents("<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>\r\n<ItemGroup>") + longstring + @" <x/></ItemGroup></Project>");

                ProjectRootElement.Open(file);
            }
            catch (InvalidProjectFileException ex)
            {
                Assert.Equal(70002, ex.LineNumber);
                Assert.Equal(2, ex.ColumnNumber);
            }
            finally
            {
                File.Delete(file);
            }
        }

        /// <summary>
        /// Tests serialization.
        /// </summary>
        [Fact]
        public void SerializationTest()
        {
            IElementLocation location = ElementLocation.Create("file", 65536, 65537);

            TranslationHelpers.GetWriteTranslator().Translate(ref location, ElementLocation.FactoryForDeserialization);
            IElementLocation deserializedLocation = null;
            TranslationHelpers.GetReadTranslator().Translate(ref deserializedLocation, ElementLocation.FactoryForDeserialization);

            Assert.Equal(location.File, deserializedLocation.File);
            Assert.Equal(location.Line, deserializedLocation.Line);
            Assert.Equal(location.Column, deserializedLocation.Column);
            Assert.Contains("RegularElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests serialization of empty location.
        /// </summary>
        [Fact]
        public void SerializationTestForEmptyLocation()
        {
            IElementLocation location = ElementLocation.EmptyLocation;

            TranslationHelpers.GetWriteTranslator().Translate(ref location, ElementLocation.FactoryForDeserialization);
            IElementLocation deserializedLocation = null;
            TranslationHelpers.GetReadTranslator().Translate(ref deserializedLocation, ElementLocation.FactoryForDeserialization);

            Assert.Equal(location.File, deserializedLocation.File);
            Assert.Equal(location.Line, deserializedLocation.Line);
            Assert.Equal(location.Column, deserializedLocation.Column);
            Assert.Contains("SmallElementLocation", deserializedLocation.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying file, line and column.
        /// </summary>
        [Fact]
        public void ConstructorWithIndicesTest_SmallElementLocation()
        {
            IElementLocation location = ElementLocation.Create("file", 65535, 65534);
            Assert.Equal("file", location.File);
            Assert.Equal(65535, location.Line);
            Assert.Equal(65534, location.Column);
            Assert.Contains("SmallElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying file, negative line, column
        /// </summary>
        [Fact]
        public void ConstructorWithNegativeIndicesTest1()
        {
            Assert.Throws<InternalErrorException>(() =>
            {
                ElementLocation.Create("file", -1, 2);
            });
        }
        /// <summary>
        /// Tests constructor specifying file, line, negative column
        /// </summary>
        [Fact]
        public void ConstructorWithNegativeIndicesTest2n()
        {
            Assert.Throws<InternalErrorException>(() =>
            {
                ElementLocation.Create("file", 1, -2);
            });
        }
        /// <summary>
        /// Tests constructor with invalid null file.
        /// </summary>
        [Fact]
        public void ConstructorTestNullFile()
        {
            IElementLocation location = ElementLocation.Create(null);
            Assert.Equal(location.File, String.Empty);
        }

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest1_SmallElementLocation()
        {
            IElementLocation location = ElementLocation.Create("file", 65535, 0);
            Assert.Equal("file", location.File);
            Assert.Equal(65535, location.Line);
            Assert.Equal(0, location.Column);
            Assert.Contains("SmallElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest2_SmallElementLocation()
        {
            IElementLocation location = ElementLocation.Create("file", 0, 65535);
            Assert.Equal("file", location.File);
            Assert.Equal(0, location.Line);
            Assert.Equal(65535, location.Column);
            Assert.Contains("SmallElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests constructor specifying only file.
        /// </summary>
        [Fact]
        public void ConstructorTest3_SmallElementLocation()
        {
            IElementLocation location = ElementLocation.Create("file", 65535, 65534);
            Assert.Equal("file", location.File);
            Assert.Equal(65535, location.Line);
            Assert.Equal(65534, location.Column);
            Assert.Contains("SmallElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Tests serialization.
        /// </summary>
        [Fact]
        public void SerializationTest_SmallElementLocation()
        {
            IElementLocation location = ElementLocation.Create("file", 65535, 2);

            TranslationHelpers.GetWriteTranslator().Translate(ref location, ElementLocation.FactoryForDeserialization);
            IElementLocation deserializedLocation = null;
            TranslationHelpers.GetReadTranslator().Translate(ref deserializedLocation, ElementLocation.FactoryForDeserialization);

            Assert.Equal(location.File, deserializedLocation.File);
            Assert.Equal(location.Line, deserializedLocation.Line);
            Assert.Equal(location.Column, deserializedLocation.Column);
            Assert.Contains("SmallElementLocation", location.GetType().FullName);
        }

        /// <summary>
        /// Test many of the getters
        /// </summary>
        [Fact]
        [Trait("Category", "netcore-osx-failing")]
        [Trait("Category", "netcore-linux-failing")]
        public void LocationStringsMedleyReadOnlyLoad()
        {
            string content = ObjectModelHelpers.CleanupFileContents(@"
            <Project ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
                    <UsingTask TaskName='t' AssemblyName='a' Condition='true'/>
                    <UsingTask TaskName='t' AssemblyFile='a' Condition='true'/>
                    <ItemDefinitionGroup Condition='true' Label='l'>
                        <m Condition='true'>  foo  bar
  </m>
                    </ItemDefinitionGroup>
                    <ItemGroup>
                        <i Include='i' Condition='true' Exclude='r'>
                            <m Condition='true'/>
                        </i>
                    </ItemGroup>
                    <PropertyGroup>
                        <p Condition='true'/>
                    </PropertyGroup>
                   <!-- A comment -->
                    <Target Name='Build' Condition='true' Inputs='i' Outputs='o'>
                        <ItemGroup>
                            <i Include='i' Condition='true' Exclude='r'>
                                <m Condition='true'/>
                            </i>
                            <i Remove='r'/>
                        </ItemGroup>
                        <PropertyGroup xml:space= 'preserve'>             <x/>
                            <p     Condition='true'/>
                        </PropertyGroup>
                        <Error Text='xyz' ContinueOnError='true' Importance='high'/>
                    </Target>
                    <Import Project='p' Condition='false'/>
                </Project>
                ");

            string readWriteLoadLocations = GetLocations(content, readOnly: false);
            string readOnlyLoadLocations = GetLocations(content, readOnly: true);

            Console.WriteLine(readWriteLoadLocations);

            Helpers.VerifyAssertLineByLine(readWriteLoadLocations, readOnlyLoadLocations);
        }

        /// <summary>
        /// Save read only fails
        /// </summary>
        [Fact]
        public void SaveReadOnly1()
        {
            Assert.Throws<InvalidOperationException>(() =>
            {
                var doc = new XmlDocumentWithLocation(loadAsReadOnly: true);
                doc.Load(_pathToCommonTargets);
                Assert.True(doc.IsReadOnly);
                doc.Save(FileUtilities.GetTemporaryFile());
            });
        }

        /// <summary>
        /// Save read only fails
        /// </summary>
        [Fact]
        [Trait("Category", "netcore-osx-failing")]
        [Trait("Category", "netcore-linux-failing")]
        public void SaveReadOnly2()
        {
            var doc = new XmlDocumentWithLocation(loadAsReadOnly: true);
            doc.Load(_pathToCommonTargets);
            Assert.True(doc.IsReadOnly);
            Assert.Throws<InvalidOperationException>(() =>
            {
                doc.Save(new MemoryStream());
            });
        }

        /// <summary>
        /// Save read only fails
        /// </summary>
        [Fact]
        [Trait("Category", "netcore-osx-failing")]
        [Trait("Category", "netcore-linux-failing")]
        public void SaveReadOnly3()
        {
            var doc = new XmlDocumentWithLocation(loadAsReadOnly: true);
            doc.Load(_pathToCommonTargets);
            Assert.True(doc.IsReadOnly);
            Assert.Throws<InvalidOperationException>(() =>
            {
                doc.Save(new StringWriter());
            });
        }

        /// <summary>
        /// Save read only fails
        /// </summary>
        [Fact]
        [Trait("Category", "netcore-osx-failing")]
        [Trait("Category", "netcore-linux-failing")]
        public void SaveReadOnly4()
        {
            var doc = new XmlDocumentWithLocation(loadAsReadOnly: true);
            doc.Load(_pathToCommonTargets);
            Assert.True(doc.IsReadOnly);
            using (XmlWriter wr = XmlWriter.Create(new FileStream(FileUtilities.GetTemporaryFileName(), FileMode.Create)))
            {
                Assert.Throws<InvalidOperationException>(() =>
                {
                    doc.Save(wr);
                });
            }
        }

        /// <summary>
        /// Get location strings for the content, loading as readonly if specified
        /// </summary>
        private string GetLocations(string content, bool readOnly)
        {
            string file = null;

            try
            {
                file = FileUtilities.GetTemporaryFileName();
                File.WriteAllText(file, content);
                var doc = new XmlDocumentWithLocation(loadAsReadOnly: readOnly);
                doc.Load(file);
                Assert.Equal(readOnly, doc.IsReadOnly);
                var allNodes = doc.SelectNodes("//*|//@*");

                string locations = String.Empty;
                foreach (var node in allNodes)
                {
                    foreach (var property in node.GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
                    {
                        if (property.Name.Equals("Location"))
                        {
                            var value = ((ElementLocation)property.GetValue(node, null));

                            if (value != null) // null means attribute is not present
                            {
                                locations += ((XmlNode)node).Name + "==" + ((XmlNode)node).Value ?? String.Empty + ":  " + value.LocationString + "\r\n";
                            }
                        }
                    }
                }

                return locations.Replace(file, "c:\\foo\\bar.csproj");
            }
            finally
            {
                File.Delete(file);
            }
        }
    }
}
