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

using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Moq;
using Xunit;
using DependencyContextCompilationOptions = Microsoft.Extensions.DependencyModel.CompilationOptions;

namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
{
    public class CSharpCompilerTest
    {
        private readonly RazorReferenceManager ReferenceManager = new TestRazorReferenceManager();

        [Theory]
        [InlineData(null)]
        [InlineData("")]
        public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationNameIsNullOrEmpty(string name)
        {
            // Arrange
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>(e => e.ApplicationName == name);
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment);

            // Act
            var options = compiler.GetDependencyContextCompilationOptions();

            // Assert
            Assert.Same(DependencyContextCompilationOptions.Default, options);
        }

        [Fact]
        public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationDoesNotHaveDependencyContext()
        {
            // Arrange
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment);

            // Act
            var options = compiler.GetDependencyContextCompilationOptions();

            // Assert
            Assert.Same(DependencyContextCompilationOptions.Default, options);
        }

        [Theory]
        [InlineData("Development", OptimizationLevel.Debug)]
        [InlineData("Staging", OptimizationLevel.Release)]
        [InlineData("Production", OptimizationLevel.Release)]
        public void Constructor_SetsOptimizationLevelBasedOnEnvironment(
            string environment,
            OptimizationLevel expected)
        {
            // Arrange
            var options = new RazorViewEngineOptions();
            var hostingEnvironment = new Mock<IWebHostEnvironment>();
            hostingEnvironment.SetupGet(e => e.EnvironmentName)
                  .Returns(environment);
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment.Object);

            // Act & Assert
            var compilationOptions = compiler.CSharpCompilationOptions;
            Assert.Equal(expected, compilationOptions.OptimizationLevel);
        }

        [Theory]
        [InlineData("Development", "DEBUG")]
        [InlineData("Staging", "RELEASE")]
        [InlineData("Production", "RELEASE")]
        public void EnsureOptions_SetsPreprocessorSymbols(string environment, string expectedConfiguration)
        {
            // Arrange
            var options = new RazorViewEngineOptions();
            var hostingEnvironment = new Mock<IWebHostEnvironment>();
            hostingEnvironment.SetupGet(e => e.EnvironmentName)
                  .Returns(environment);
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment.Object);

            // Act & Assert
            var parseOptions = compiler.ParseOptions;
            Assert.Equal(new[] { expectedConfiguration }, parseOptions.PreprocessorSymbolNames);
        }

        [Fact]
        public void EnsureOptions_ConfiguresDefaultCompilationOptions()
        {
            // Arrange
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>(h => h.EnvironmentName == "Development");
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment);

            // Act & Assert
            var compilationOptions = compiler.CSharpCompilationOptions;
            Assert.False(compilationOptions.AllowUnsafe);
            Assert.Equal(ReportDiagnostic.Default, compilationOptions.GeneralDiagnosticOption);
            Assert.Equal(OptimizationLevel.Debug, compilationOptions.OptimizationLevel);
            Assert.Collection(compilationOptions.SpecificDiagnosticOptions.OrderBy(d => d.Key),
                item =>
                {
                    Assert.Equal("CS1701", item.Key);
                    Assert.Equal(ReportDiagnostic.Suppress, item.Value);
                },
                item =>
                {
                    Assert.Equal("CS1702", item.Key);
                    Assert.Equal(ReportDiagnostic.Suppress, item.Value);
                },
                item =>
                {
                    Assert.Equal("CS1705", item.Key);
                    Assert.Equal(ReportDiagnostic.Suppress, item.Value);
                });
        }

        [Fact]
        public void EnsureOptions_ConfiguresDefaultParseOptions()
        {
            // Arrange
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>(h => h.EnvironmentName == "Development");
            var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment);

            // Act & Assert
            var parseOptions = compiler.ParseOptions;
            Assert.Equal(LanguageVersion.CSharp8, parseOptions.LanguageVersion);
            Assert.Equal(new[] { "DEBUG" }, parseOptions.PreprocessorSymbolNames);
        }

        [Fact]
        public void Constructor_ConfiguresPreprocessorSymbolNames()
        {
            // Arrange
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();
            var dependencyContextOptions = GetDependencyContextCompilationOptions("SOME_TEST_DEFINE");

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var parseOptions = compiler.ParseOptions;
            Assert.Contains("SOME_TEST_DEFINE", parseOptions.PreprocessorSymbolNames);
        }

        [Fact]
        public void Constructor_ConfiguresLanguageVersion()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(languageVersion: "7.1");
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var compilationOptions = compiler.ParseOptions;
            Assert.Equal(LanguageVersion.CSharp7_1, compilationOptions.LanguageVersion);
        }


        [Fact]
        public void EmitOptions_ReadsDebugTypeFromDependencyContext()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "portable");
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var emitOptions = compiler.EmitOptions;
            Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat);
            Assert.True(compiler.EmitPdb);
        }

        [Fact]
        public void EmitOptions_SetsDebugInformationFormatToPortable_WhenDebugTypeIsEmbedded()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "embedded");
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var emitOptions = compiler.EmitOptions;
            Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat);
            Assert.True(compiler.EmitPdb);
        }

        [Fact]
        public void EmitOptions_DoesNotSetEmitPdb_IfDebugTypeIsNone()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "none");
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            Assert.False(compiler.EmitPdb);
        }

        [Fact]
        public void Constructor_ConfiguresAllowUnsafe()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(allowUnsafe: true);
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var compilationOptions = compiler.CSharpCompilationOptions;
            Assert.True(compilationOptions.AllowUnsafe);
        }

        [Fact]
        public void Constructor_SetsDiagnosticOption()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(warningsAsErrors: true);
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var compilationOptions = compiler.CSharpCompilationOptions;
            Assert.Equal(ReportDiagnostic.Error, compilationOptions.GeneralDiagnosticOption);
        }

        [Fact]
        public void Constructor_SetsOptimizationLevel()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions(optimize: true);
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();

            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var compilationOptions = compiler.CSharpCompilationOptions;
            Assert.Equal(OptimizationLevel.Release, compilationOptions.OptimizationLevel);
        }

        [Fact]
        public void Constructor_SetsDefines()
        {
            // Arrange
            var dependencyContextOptions = GetDependencyContextCompilationOptions("MyDefine");
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();
            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act & Assert
            var parseOptions = compiler.ParseOptions;
            Assert.Equal(new[] { "MyDefine", "RELEASE" }, parseOptions.PreprocessorSymbolNames);
        }

        [Fact]
        public void Compile_UsesApplicationsCompilationSettings_ForParsingAndCompilation()
        {
            // Arrange
            var content = "public class Test {}";
            var define = "MY_CUSTOM_DEFINE";
            var dependencyContextOptions = GetDependencyContextCompilationOptions(define);
            var hostingEnvironment = Mock.Of<IWebHostEnvironment>();
            var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions);

            // Act
            var syntaxTree = compiler.CreateSyntaxTree(SourceText.From(content));

            // Assert
            Assert.Contains(define, syntaxTree.Options.PreprocessorSymbolNames);
        }

        private static DependencyContextCompilationOptions GetDependencyContextCompilationOptions(
            string define = null,
            string languageVersion = null,
            string platform = null,
            bool? allowUnsafe = null,
            bool? warningsAsErrors = null,
            bool? optimize = null,
            string keyFile = null,
            bool? delaySign = null,
            bool? publicSign = null,
            string debugType = null)
        {
            return new DependencyContextCompilationOptions(
                new[] { define },
                languageVersion,
                platform,
                allowUnsafe,
                warningsAsErrors,
                optimize,
                keyFile,
                delaySign,
                publicSign,
                debugType,
                emitEntryPoint: null,
                generateXmlDocumentation: null);
        }

        private class TestCSharpCompiler : CSharpCompiler
        {
            private readonly DependencyContextCompilationOptions _options;

            public TestCSharpCompiler(
                RazorReferenceManager referenceManager,
                IWebHostEnvironment hostingEnvironment,
                DependencyContextCompilationOptions options)
                : base(referenceManager, hostingEnvironment)
            {
                _options = options;
            }

            protected internal override DependencyContextCompilationOptions GetDependencyContextCompilationOptions()
                => _options;
        }
    }
}
