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

namespace Microsoft.Extensions.ApiDescription.Client
{
    public class GetOpenApiReferenceMetadataTest
    {
        [Fact]
        public void Execute_AddsExpectedMetadata()
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var @namespace = "Console.Client";
            var outputPath = Path.Combine("obj", "NSwagClient.cs");
            var inputMetadata = new Dictionary<string, string> { { "CodeGenerator", "NSwagCSharp" } };
            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = @namespace,
                OutputDirectory = "obj",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", "NSwagClient" },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", outputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={outputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName=NSwagClient"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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);
        }

        [Fact]
        public void Execute_DoesNotOverrideClassName()
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var className = "ThisIsClassy";
            var @namespace = "Console.Client";
            var outputPath = Path.Combine("obj", $"NSwagClient.cs");
            var inputMetadata = new Dictionary<string, string>
            {
                { "CodeGenerator", "NSwagCSharp" },
                { "ClassName", className },
            };

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = @namespace,
                OutputDirectory = "obj",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", outputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={outputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName={className}"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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);
        }

        [Fact]
        public void Execute_DoesNotOverrideNamespace()
        {
            // Arrange
            var defaultNamespace = "Console.Client";
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var @namespace = "NotConsole.NotClient";
            var outputPath = Path.Combine("obj", "NSwagClient.cs");
            var inputMetadata = new Dictionary<string, string>
            {
                { "CodeGenerator", "NSwagCSharp" },
                { "Namespace", @namespace },
            };

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = defaultNamespace,
                OutputDirectory = "obj",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", "NSwagClient" },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", outputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={outputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName=NSwagClient"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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);
        }

        [Fact]
        public void Execute_DoesNotOverrideOutputPath_IfRooted()
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var className = "ThisIsClassy";
            var @namespace = "Console.Client";
            var outputPath = Path.Combine(Path.GetTempPath(), $"{className}.cs");
            var inputMetadata = new Dictionary<string, string>
            {
                { "CodeGenerator", "NSwagCSharp" },
                { "OutputPath", outputPath }
            };

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = @namespace,
                OutputDirectory = "bin",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", outputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={outputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName={className}"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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);
        }

        [Fact]
        public void Execute_LogsError_IfCodeGeneratorMissing()
        {
            // Arrange
            var identity1 = Path.Combine("TestProjects", "files", "NSwag.json");
            var identity2 = Path.Combine("TestProjects", "files", "swashbuckle.json");
            var error1 = Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", "OpenApiReference", identity1);
            var error2 = Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", "OpenApiProjectReference", identity2);
            var @namespace = "Console.Client";
            var inputMetadata1 = new Dictionary<string, string>
            {
                { "ExtraMetadata", "this is extra" },
            };
            var inputMetadata2 = new Dictionary<string, string>
            {
                { "Options", "-quiet" },
                { "SourceProject", "ConsoleProject.csproj" },
            };

            var buildEngine = new MockBuildEngine();
            var task = new GetOpenApiReferenceMetadata
            {
                BuildEngine = buildEngine,
                Extension = ".cs",
                Inputs = new[]
                {
                    new TaskItem(identity1, inputMetadata1),
                    new TaskItem(identity2, inputMetadata2),
                },
                Namespace = @namespace,
                OutputDirectory = "obj",
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.False(result);
            Assert.True(task.Log.HasLoggedErrors);
            Assert.Equal(2, buildEngine.Errors);
            Assert.Equal(0, buildEngine.Messages);
            Assert.Equal(0, buildEngine.Warnings);
            Assert.Contains(error1, buildEngine.Log, StringComparison.OrdinalIgnoreCase);
            Assert.Contains(error2, buildEngine.Log, StringComparison.OrdinalIgnoreCase);
        }

        [Fact]
        public void Execute_LogsError_IfOutputPathDuplicated()
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var codeGenerator = "NSwagCSharp";
            var error = Resources.FormatDuplicateFileOutputPaths(Path.Combine("obj", "NSwagClient.cs"));
            var @namespace = "Console.Client";
            var inputMetadata1 = new Dictionary<string, string>
            {
                { "CodeGenerator", codeGenerator },
                { "ExtraMetadata", "this is extra" },
            };
            var inputMetadata2 = new Dictionary<string, string>
            {
                { "CodeGenerator", codeGenerator },
                { "Options", "-quiet" },
            };

            var buildEngine = new MockBuildEngine();
            var task = new GetOpenApiReferenceMetadata
            {
                BuildEngine = buildEngine,
                Extension = ".cs",
                Inputs = new[]
                {
                    new TaskItem(identity, inputMetadata1),
                    new TaskItem(identity, inputMetadata2),
                },
                Namespace = @namespace,
                OutputDirectory = "obj",
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.False(result);
            Assert.True(task.Log.HasLoggedErrors);
            Assert.Equal(1, buildEngine.Errors);
            Assert.Equal(0, buildEngine.Messages);
            Assert.Equal(0, buildEngine.Warnings);
            Assert.Contains(error, buildEngine.Log, StringComparison.OrdinalIgnoreCase);
        }

        [Fact]
        public void Execute_SetsClassName_BasedOnOutputPath()
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var className = "ThisIsClassy";
            var @namespace = "Console.Client";
            var outputPath = $"{className}.cs";
            var expectedOutputPath = Path.Combine("bin", outputPath);
            var inputMetadata = new Dictionary<string, string>
            {
                { "CodeGenerator", "NSwagCSharp" },
                { "OutputPath", outputPath }
            };

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = @namespace,
                OutputDirectory = "bin",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", expectedOutputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={expectedOutputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName={className}"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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]
        [InlineData("aa-bb.cs", "aa_bb")]
        [InlineData("aa.bb.cc.ts", "aa_bb_cc")]
        [InlineData("aa\u20DF\u20DF.tsx", "aa__")] // UnicodeCategory.EnclosingMark (combining enclosing diamond)
        [InlineData("aa\u2005bb\u2005cc.cs", "aa_bb_cc")] // UnicodeCategory.SpaceSeparator (four-per-em space)
        [InlineData("aa\u0096\u0096bb.cs", "aa__bb")] // UnicodeCategory.Control (start of guarded area)
        [InlineData("aa\uFF1C\uFF1C\uFF1Cbb.cs", "aa___bb")] // UnicodeCategory.MathSymbol (fullwidth less-than sign)
        public void Execute_SetsClassName_BasedOnSanitizedOutputPath(string outputPath, string className)
        {
            // Arrange
            var identity = Path.Combine("TestProjects", "files", "NSwag.json");
            var @namespace = "Console.Client";
            var expectedOutputPath = Path.Combine("bin", outputPath);
            var inputMetadata = new Dictionary<string, string>
            {
                { "CodeGenerator", "NSwagCSharp" },
                { "OutputPath", outputPath }
            };

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[] { new TaskItem(identity, inputMetadata) },
                Namespace = @namespace,
                OutputDirectory = "bin",
            };

            var expectedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className },
                { "CodeGenerator", "NSwagCSharp" },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity },
                { "OutputPath", expectedOutputPath },
                {
                    "SerializedMetadata",
                    $"Identity={identity}|FirstForGenerator=true|" +
                    $"CodeGenerator=NSwagCSharp|OutputPath={expectedOutputPath}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity}|ClassName={className}"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            var output = Assert.Single(task.Outputs);
            Assert.Equal(identity, output.ItemSpec);
            var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.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);
        }

        [Fact]
        public void Execute_SetsFirstForGenerator_UsesCorrectExtension()
        {
            // Arrange
            var identity12 = Path.Combine("TestProjects", "files", "NSwag.json");
            var identity3 = Path.Combine("TestProjects", "files", "swashbuckle.json");
            var className12 = "NSwagClient";
            var className3 = "swashbuckleClient";
            var codeGenerator13 = "NSwagCSharp";
            var codeGenerator2 = "NSwagTypeScript";
            var inputMetadata1 = new Dictionary<string, string> { { "CodeGenerator", codeGenerator13 } };
            var inputMetadata2 = new Dictionary<string, string> { { "CodeGenerator", codeGenerator2 } };
            var inputMetadata3 = new Dictionary<string, string> { { "CodeGenerator", codeGenerator13 } };
            var @namespace = "Console.Client";
            var outputPath1 = Path.Combine("obj", $"{className12}.cs");
            var outputPath2 = Path.Combine("obj", $"{className12}.ts");
            var outputPath3 = Path.Combine("obj", $"{className3}.cs");

            var task = new GetOpenApiReferenceMetadata
            {
                Extension = ".cs",
                Inputs = new[]
                {
                    new TaskItem(identity12, inputMetadata1),
                    new TaskItem(identity12, inputMetadata2),
                    new TaskItem(identity3, inputMetadata3),
                },
                Namespace = @namespace,
                OutputDirectory = "obj",
            };

            var expectedMetadata1 = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className12 },
                { "CodeGenerator", codeGenerator13 },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity12 },
                { "OutputPath", outputPath1 },
                {
                    "SerializedMetadata",
                    $"Identity={identity12}|FirstForGenerator=true|" +
                    $"CodeGenerator={codeGenerator13}|OutputPath={outputPath1}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity12}|ClassName={className12}"
                },
            };
            var expectedMetadata2 = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className12 },
                { "CodeGenerator", codeGenerator2 },
                { "FirstForGenerator", "true" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity12 },
                { "OutputPath", outputPath2 },
                {
                    "SerializedMetadata",
                    $"Identity={identity12}|FirstForGenerator=true|" +
                    $"CodeGenerator={codeGenerator2}|OutputPath={outputPath2}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity12}|ClassName={className12}"
                },
            };
            var expectedMetadata3 = new SortedDictionary<string, string>(StringComparer.Ordinal)
            {
                { "ClassName", className3 },
                { "CodeGenerator", codeGenerator13 },
                { "FirstForGenerator", "false" },
                { "Namespace", @namespace },
                { "OriginalItemSpec", identity3 },
                { "OutputPath", outputPath3 },
                {
                    "SerializedMetadata",
                    $"Identity={identity3}|FirstForGenerator=false|" +
                    $"CodeGenerator={codeGenerator13}|OutputPath={outputPath3}|Namespace={@namespace}|" +
                    $"OriginalItemSpec={identity3}|ClassName={className3}"
                },
            };

            // Act
            var result = task.Execute();

            // Assert
            Assert.True(result);
            Assert.False(task.Log.HasLoggedErrors);
            Assert.Collection(
                task.Outputs,
                output =>
                {
                    Assert.Equal(identity12, output.ItemSpec);
                    var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.CloneCustomMetadata());
                    var orderedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
                    foreach (var key in metadata.Keys)
                    {
                        orderedMetadata.Add(key, metadata[key]);
                    }

                    Assert.Equal(expectedMetadata1, orderedMetadata);
                },
                output =>
                {
                    Assert.Equal(identity12, output.ItemSpec);
                    var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.CloneCustomMetadata());
                    var orderedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
                    foreach (var key in metadata.Keys)
                    {
                        orderedMetadata.Add(key, metadata[key]);
                    }

                    Assert.Equal(expectedMetadata2, orderedMetadata);
                },
                output =>
                {
                    Assert.Equal(identity3, output.ItemSpec);
                    var metadata = Assert.IsAssignableFrom<IDictionary<string, string>>(output.CloneCustomMetadata());
                    var orderedMetadata = new SortedDictionary<string, string>(StringComparer.Ordinal);
                    foreach (var key in metadata.Keys)
                    {
                        orderedMetadata.Add(key, metadata[key]);
                    }

                    Assert.Equal(expectedMetadata3, orderedMetadata);
                });
        }
    }
}
