// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
{
    public class CompilerFailedExceptionFactoryTest
    {
        [Fact]
        public void GetCompilationFailedResult_ReadsRazorErrorsFromPage()
        {
            // Arrange
            var viewPath = "/Views/Home/Index.cshtml";

            var fileSystem = new VirtualRazorProjectFileSystem();
            var projectItem = new TestRazorProjectItem(viewPath, "<span name=\"@(User.Id\">");

            var razorEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine;
            var codeDocument = GetCodeDocument(projectItem);

            // Act
            razorEngine.Process(codeDocument);
            var csharpDocument = codeDocument.GetCSharpDocument();
            var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);

            // Assert
            var failure = Assert.Single(compilationResult.CompilationFailures);
            Assert.Equal(viewPath, failure.SourceFilePath);
            var orderedMessages = failure.Messages.OrderByDescending(messages => messages.Message);
            Assert.Collection(orderedMessages,
                message => Assert.StartsWith(
                    @"Unterminated string literal.",
                    message.Message),
                message => Assert.StartsWith(
                    @"The explicit expression block is missing a closing "")"" character.",
                    message.Message));
        }

        [Fact]
        public void GetCompilationFailedResult_WithMissingReferences()
        {
            // Arrange
            var expected = "One or more compilation references may be missing. If you're seeing this in a published application, set 'CopyRefAssembliesToPublishDirectory' to true in your project file to ensure files in the refs directory are published.";
            var compilation = CSharpCompilation.Create("Test", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
            var syntaxTree = CSharpSyntaxTree.ParseText("@class Test { public string Test { get; set; } }");
            compilation = compilation.AddSyntaxTrees(syntaxTree);
            var emitResult = compilation.Emit(new MemoryStream());

            // Act
            var exception = CompilationFailedExceptionFactory.Create(
                RazorCodeDocument.Create(RazorSourceDocument.Create("Test", "Index.cshtml"), Enumerable.Empty<RazorSourceDocument>()),
                syntaxTree.ToString(),
                "Test",
                emitResult.Diagnostics);

            // Assert
            Assert.Collection(
                exception.CompilationFailures,
                failure => Assert.Equal(expected, failure.FailureSummary));
        }

        [Fact]
        public void GetCompilationFailedResult_UsesPhysicalPath()
        {
            // Arrange
            var viewPath = "/Views/Home/Index.cshtml";
            var physicalPath = @"x:\myapp\views\home\index.cshtml";

            var projectItem = new TestRazorProjectItem(viewPath, "<span name=\"@(User.Id\">", physicalPath: physicalPath);

            var codeDocument = GetCodeDocument(projectItem);
            var csharpDocument = codeDocument.GetCSharpDocument();

            // Act
            var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);

            // Assert
            var failure = Assert.Single(compilationResult.CompilationFailures);
            Assert.Equal(physicalPath, failure.SourceFilePath);
        }

        [Fact]
        public void GetCompilationFailedResult_ReadsContentFromSourceDocuments()
        {
            // Arrange
            var viewPath = "/Views/Home/Index.cshtml";
            var fileContent =
@"
@if (User.IsAdmin)
{
    <span>
}
</span>";

            var projectItem = new TestRazorProjectItem(viewPath, fileContent);
            var codeDocument = GetCodeDocument(projectItem);
            var csharpDocument = codeDocument.GetCSharpDocument();

            // Act
            var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);

            // Assert
            var failure = Assert.Single(compilationResult.CompilationFailures);
            Assert.Equal(fileContent, failure.SourceFileContent);
        }

        [Fact]
        public void GetCompilationFailedResult_ReadsContentFromImports()
        {
            // Arrange
            var viewPath = "/Views/Home/Index.cshtml";
            var importsPath = "/Views/_MyImports.cshtml";
            var fileContent = "@ ";
            var importsContent = "@(abc";

            var projectItem = new TestRazorProjectItem(viewPath, fileContent);
            var importsItem = new TestRazorProjectItem(importsPath, importsContent);
            var codeDocument = GetCodeDocument(projectItem, importsItem);
            var csharpDocument = codeDocument.GetCSharpDocument();

            // Act
            var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics);

            // Assert
            Assert.Collection(
                compilationResult.CompilationFailures,
                failure =>
                {
                    Assert.Equal(viewPath, failure.SourceFilePath);
                    Assert.Collection(failure.Messages,
                        message =>
                        {
                            Assert.Equal(@"A space or line break was encountered after the ""@"" character.  Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.",
                                message.Message);
                        });
                },
                failure =>
                {
                    Assert.Equal(importsPath, failure.SourceFilePath);
                    Assert.Collection(failure.Messages,
                        message =>
                        {
                            Assert.Equal(@"The explicit expression block is missing a closing "")"" character.  Make sure you have a matching "")"" character for all the ""("" characters within this block, and that none of the "")"" characters are being interpreted as markup.",
                            message.Message);
                        });
                });
        }

        [Fact]
        public void GetCompilationFailedResult_GroupsMessages()
        {
            // Arrange
            var viewPath = "views/index.razor";
            var viewImportsPath = "views/global.import.cshtml";
            var codeDocument = RazorCodeDocument.Create(
                Create(viewPath, "View Content"),
                new[] { Create(viewImportsPath, "Global Import Content") });
            var diagnostics = new[]
            {
                GetRazorDiagnostic("message-1", new SourceLocation(1, 2, 17), length: 1),
                GetRazorDiagnostic("message-2", new SourceLocation(viewPath, 1, 4, 6), length: 7),
                GetRazorDiagnostic("message-3", SourceLocation.Undefined, length: -1),
                GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4),
            };

            // Act
            var result = CompilationFailedExceptionFactory.Create(codeDocument, diagnostics);

            // Assert
            Assert.Collection(result.CompilationFailures,
            failure =>
            {
                Assert.Equal(viewPath, failure.SourceFilePath);
                Assert.Equal("View Content", failure.SourceFileContent);
                Assert.Collection(failure.Messages,
                    message =>
                    {
                        Assert.Equal(diagnostics[0].GetMessage(CultureInfo.CurrentCulture), message.Message);
                        Assert.Equal(viewPath, message.SourceFilePath);
                        Assert.Equal(3, message.StartLine);
                        Assert.Equal(17, message.StartColumn);
                        Assert.Equal(3, message.EndLine);
                        Assert.Equal(18, message.EndColumn);
                    },
                    message =>
                    {
                        Assert.Equal(diagnostics[1].GetMessage(CultureInfo.CurrentCulture), message.Message);
                        Assert.Equal(viewPath, message.SourceFilePath);
                        Assert.Equal(5, message.StartLine);
                        Assert.Equal(6, message.StartColumn);
                        Assert.Equal(5, message.EndLine);
                        Assert.Equal(13, message.EndColumn);
                    },
                    message =>
                    {
                        Assert.Equal(diagnostics[2].GetMessage(CultureInfo.CurrentCulture), message.Message);
                        Assert.Equal(viewPath, message.SourceFilePath);
                        Assert.Equal(0, message.StartLine);
                        Assert.Equal(-1, message.StartColumn);
                        Assert.Equal(0, message.EndLine);
                        Assert.Equal(-2, message.EndColumn);
                    });
            },
            failure =>
            {
                Assert.Equal(viewImportsPath, failure.SourceFilePath);
                Assert.Equal("Global Import Content", failure.SourceFileContent);
                Assert.Collection(failure.Messages,
                    message =>
                    {
                        Assert.Equal(diagnostics[3].GetMessage(CultureInfo.CurrentCulture), message.Message);
                        Assert.Equal(viewImportsPath, message.SourceFilePath);
                        Assert.Equal(4, message.StartLine);
                        Assert.Equal(8, message.StartColumn);
                        Assert.Equal(4, message.EndLine);
                        Assert.Equal(12, message.EndColumn);
                    });
            });
        }

        [Fact]
        public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages()
        {
            // Arrange
            var viewPath = "Views/Home/Index";
            var generatedCodeFileName = "Generated Code";
            var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("view-content", viewPath));
            var assemblyName = "random-assembly-name";

            var diagnostics = new[]
            {
                Diagnostic.Create(
                    GetRoslynDiagnostic("message-1"),
                    Location.Create(
                        viewPath,
                        new TextSpan(10, 5),
                        new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))),
                Diagnostic.Create(
                    GetRoslynDiagnostic("message-2"),
                    Location.Create(
                        assemblyName,
                        new TextSpan(1, 6),
                        new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))),
                Diagnostic.Create(
                    GetRoslynDiagnostic("message-3"),
                    Location.Create(
                        viewPath,
                        new TextSpan(40, 50),
                        new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))),
            };

            // Act
            var compilationResult = CompilationFailedExceptionFactory.Create(
                codeDocument,
                "compilation-content",
                assemblyName,
                diagnostics);

            // Assert
            Assert.Collection(compilationResult.CompilationFailures,
                failure =>
                {
                    Assert.Equal(viewPath, failure.SourceFilePath);
                    Assert.Equal("view-content", failure.SourceFileContent);
                    Assert.Collection(failure.Messages,
                        message =>
                        {
                            Assert.Equal("message-1", message.Message);
                            Assert.Equal(viewPath, message.SourceFilePath);
                            Assert.Equal(11, message.StartLine);
                            Assert.Equal(2, message.StartColumn);
                            Assert.Equal(11, message.EndLine);
                            Assert.Equal(3, message.EndColumn);
                        },
                        message =>
                        {
                            Assert.Equal("message-3", message.Message);
                            Assert.Equal(viewPath, message.SourceFilePath);
                            Assert.Equal(31, message.StartLine);
                            Assert.Equal(6, message.StartColumn);
                            Assert.Equal(41, message.EndLine);
                            Assert.Equal(13, message.EndColumn);
                        });
                },
                failure =>
                {
                    Assert.Equal(generatedCodeFileName, failure.SourceFilePath);
                    Assert.Equal("compilation-content", failure.SourceFileContent);
                    Assert.Collection(failure.Messages,
                        message =>
                        {
                            Assert.Equal("message-2", message.Message);
                            Assert.Equal(assemblyName, message.SourceFilePath);
                            Assert.Equal(2, message.StartLine);
                            Assert.Equal(3, message.StartColumn);
                            Assert.Equal(4, message.EndLine);
                            Assert.Equal(5, message.EndColumn);
                        });
                });
        }

        private static RazorSourceDocument Create(string path, string template)
        {
            var stream = new MemoryStream(Encoding.UTF8.GetBytes(template));
            return RazorSourceDocument.ReadFrom(stream, path);
        }

        private static RazorDiagnostic GetRazorDiagnostic(string message, SourceLocation sourceLocation, int length)
        {
            var diagnosticDescriptor = new RazorDiagnosticDescriptor("test-id", () => message, RazorDiagnosticSeverity.Error);
            var sourceSpan = new SourceSpan(sourceLocation, length);

            return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan);
        }

        private static DiagnosticDescriptor GetRoslynDiagnostic(string messageFormat)
        {
            return new DiagnosticDescriptor(
                id: "someid",
                title: "sometitle",
                messageFormat: messageFormat,
                category: "some-category",
                defaultSeverity: DiagnosticSeverity.Error,
                isEnabledByDefault: true);
        }

        private static RazorCodeDocument GetCodeDocument(TestRazorProjectItem projectItem, TestRazorProjectItem imports = null)
        {
            var sourceDocument = RazorSourceDocument.ReadFrom(projectItem);
            var fileSystem = new VirtualRazorProjectFileSystem();
            fileSystem.Add(projectItem);

            var codeDocument = RazorCodeDocument.Create(sourceDocument);

            if (imports != null)
            {
                fileSystem.Add(imports);
                codeDocument = RazorCodeDocument.Create(sourceDocument, new[] { RazorSourceDocument.ReadFrom(imports) });
            }

            var razorEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine;

            razorEngine.Process(codeDocument);
            return codeDocument;
        }

    }
}
