// 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 Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Moq;
using Xunit;

namespace Microsoft.Extensions.ApiDescription.Client
{
    // ItemSpec values always have '\\' converted to '/' on input when running on non-Windows. It is not possible to
    // retrieve the original (unconverted) item spec value. In other respects, item spec values are treated identically
    // to custom metadata values.
    //
    // ITaskItem members aka the implicitly-implemented methods and properties in TaskItem expect _escaped_ values on
    // input and return _literal_ values. This includes TaskItem constructors and CloneCustomMetadata() (which returns
    // a new dictionary containing literal values). TaskItem stores all values in their escaped form.
    //
    // Added ITaskItem2 members e.g. CloneCustomMetadataEscaped(), GetMetadataValueEscaped(...) and
    // EvaluatedIncludeEscaped return escaped values. Of all TaskItem methods, only SetMetadataValueLiteral(...)
    // accepts a literal input value.
    //
    // Metadata names are never escaped.
    //
    // MetadataSerializer expects literal values on input.
    public class MetadataSerializerTest
    {
        // Maps literal to escaped values.
        public static TheoryData<string, string> EscapedValuesMapping { get; } = new TheoryData<string, string>
        {
          { "No escaping necessary for =.", "No escaping necessary for =." },
          { "Value needs escaping? (yes)", "Value needs escaping%3f %28yes%29" },
          { "$ comes earlier; @ comes later.", "%24 comes earlier%3b %40 comes later." },
          {
            "A '%' *character* needs escaping %-escaping.",
            "A %27%25%27 %2acharacter%2a needs escaping %25-escaping."
          },
        };

        public static TheoryData<string> EscapedValues
        {
            get
            {
                var result = new TheoryData<string>();
                foreach (var entry in EscapedValuesMapping)
                {
                    result.Add((string)entry[1]);
                }

                return result;
            }
        }

        public static TheoryData<string> LiteralValues
        {
            get
            {
                var result = new TheoryData<string>();
                foreach (var entry in EscapedValuesMapping)
                {
                    result.Add((string)entry[0]);
                }

                return result;
            }
        }

        [Theory]
        [MemberData(nameof(LiteralValues))]
        public void SetMetadata_UpdatesTaskAsExpected(string value)
        {
            // Arrange
            var item = new TaskItem("My Identity");
            var key = "My key";

            // Act
            MetadataSerializer.SetMetadata(item, key, value);

            // Assert
            Assert.Equal(value, item.GetMetadata(key));
        }

        [Theory]
        [MemberData(nameof(EscapedValuesMapping))]
        public void SetMetadata_UpdatesTaskAsExpected_WithLegacyItem(string value, string escapedValue)
        {
            // Arrange
            var item = new Mock<ITaskItem>(MockBehavior.Strict);
            var key = "My key";
            item.Setup(i => i.SetMetadata(key, escapedValue)).Verifiable();

            // Act
            MetadataSerializer.SetMetadata(item.Object, key, value);

            // Assert
            item.Verify(i => i.SetMetadata(key, escapedValue), Times.Once);
        }

        [Fact]
        public void DeserializeMetadata_ReturnsExpectedTask()
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var input = $"Identity={identity}|ClassName=azureMonitorClient|" +
                "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" +
                "Options=|OriginalItemSpec=../files/azureMonitor.json|" +
                "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs";

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", "azureMonitorClient" },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", "ConsoleClient" },
                { "Options", "" },
                { "OriginalItemSpec", identity },
                { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
            };

            // Act
            var item = MetadataSerializer.DeserializeMetadata(input);

            // Assert
            Assert.Equal(identity, item.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(item.CloneCustomMetadata());

            // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator.
            var orderedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
            foreach (var key in metadata.Keys)
            {
                orderedMetadata.Add(key, metadata[key]);
            }

            Assert.Equal(expectedMetadata, orderedMetadata);

        }

        [Theory]
        [MemberData(nameof(EscapedValuesMapping))]
        public void DeserializeMetadata_ReturnsExpectedTask_WhenEscaping(string value, string escapedValue)
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var input = $"Identity={identity}|Value={escapedValue}";

            // Act
            var item = MetadataSerializer.DeserializeMetadata(input);

            // Assert
            Assert.Equal(identity, item.ItemSpec);
            Assert.Equal(value, item.GetMetadata("Value"));
        }

        [Theory]
        [MemberData(nameof(EscapedValuesMapping))]
        public void DeserializeMetadata_ReturnsExpectedTask_WhenEscapingIdentity(string value, string escapedValue)
        {
            // Arrange
            var input = $"Identity={escapedValue}|Value=a value";

            // Act
            var item = MetadataSerializer.DeserializeMetadata(input);

            // Assert
            Assert.Equal(value, item.ItemSpec);
            Assert.Equal("a value", item.GetMetadata("Value"));
        }

        [Fact]
        public void SerializeMetadata_ReturnsExpectedString()
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", "azureMonitorClient" },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", "ConsoleClient" },
                { "Options", "" },
                { "OriginalItemSpec", identity },
                { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
            };

            var input = new TaskItem(identity, metadata);
            var expectedResult = $"Identity={identity}|FirstForGenerator=true|" +
                "Options=|CodeGenerator=NSwagCSharp|" +
                "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs|" +
                "Namespace=ConsoleClient|OriginalItemSpec=../files/azureMonitor.json|ClassName=azureMonitorClient";

            // Act
            var result = MetadataSerializer.SerializeMetadata(input);

            // Assert
            Assert.Equal(expectedResult, result);
        }

        [Theory]
        [MemberData(nameof(EscapedValues))]
        public void SerializeMetadata_ReturnsExpectedString_WhenEscaping(string escapedValue)
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var expectedResult = $"Identity={identity}|Value={escapedValue}";
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal) { { "Value", escapedValue } };
            var input = new TaskItem(identity, metadata);

            // Act
            var result = MetadataSerializer.SerializeMetadata(input);

            // Assert
            Assert.Equal(expectedResult, result);
        }

        [Theory]
        [MemberData(nameof(EscapedValues))]
        public void SerializeMetadata_ReturnsExpectedString_WhenEscapingIdentity(string escapedValue)
        {
            // Arrange
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal) { { "Value", "a value" } };
            var expectedResult = $"Identity={escapedValue}|Value=a value";
            var input = new TaskItem(escapedValue, metadata);

            // Act
            var result = MetadataSerializer.SerializeMetadata(input);

            // Assert
            Assert.Equal(expectedResult, result);
        }

        [Fact]
        public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem()
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", "azureMonitorClient" },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", "ConsoleClient" },
                { "Options", "" },
                { "OriginalItemSpec", identity },
                { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" },
            };

            var input = new Mock<ITaskItem>(MockBehavior.Strict);
            input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable();
            input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();

            var expectedResult = $"Identity={identity}|ClassName=azureMonitorClient|" +
                "CodeGenerator=NSwagCSharp|FirstForGenerator=true|Namespace=ConsoleClient|" +
                "Options=|OriginalItemSpec=../files/azureMonitor.json|" +
                "OutputPath=C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs";

            // Act
            var result = MetadataSerializer.SerializeMetadata(input.Object);

            // Assert
            Assert.Equal(expectedResult, result);
            input.VerifyGet(i => i.ItemSpec, Times.Once);
            input.Verify(i => i.CloneCustomMetadata(), Times.Once);
        }

        [Theory]
        [MemberData(nameof(EscapedValuesMapping))]
        public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscaping(
            string value,
            string escapedValue)
        {
            // Arrange
            var identity = "../files/azureMonitor.json";
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal) { { "Value", value } };
            var input = new Mock<ITaskItem>(MockBehavior.Strict);
            input.SetupGet(i => i.ItemSpec).Returns(identity).Verifiable();
            input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();

            var expectedResult = $"Identity={identity}|Value={escapedValue}";

            // Act
            var result = MetadataSerializer.SerializeMetadata(input.Object);

            // Assert
            Assert.Equal(expectedResult, result);
            input.VerifyGet(i => i.ItemSpec, Times.Once);
            input.Verify(i => i.CloneCustomMetadata(), Times.Once);
        }

        [Theory]
        [MemberData(nameof(EscapedValuesMapping))]
        public void SerializeMetadata_ReturnsExpectedString_WithLegacyItem_WhenEscapingIdentity(
            string value,
            string escapedValue)
        {
            // Arrange
            var metadata = new SortedDictionary<string, string>(StringComparer.Ordinal) { { "Value", "a value" } };
            var input = new Mock<ITaskItem>(MockBehavior.Strict);
            input.SetupGet(i => i.ItemSpec).Returns(value).Verifiable();
            input.Setup(i => i.CloneCustomMetadata()).Returns(metadata).Verifiable();

            var expectedResult = $"Identity={escapedValue}|Value=a value";

            // Act
            var result = MetadataSerializer.SerializeMetadata(input.Object);

            // Assert
            Assert.Equal(expectedResult, result);
            input.VerifyGet(i => i.ItemSpec, Times.Once);
            input.Verify(i => i.CloneCustomMetadata(), Times.Once);
        }
    }
}
