// 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);
    }
}
