﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Diagnostics.TypeStyle;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Simplification;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Remote.Testing;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.VisualBasic.CodeStyle;
using Microsoft.CodeAnalysis.VisualBasic.UseNullPropagation;
using Microsoft.CodeAnalysis.Workspaces.Diagnostics;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Roslyn.VisualStudio.Next.UnitTests.Remote
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.RemoteHost)]
    public class VisualStudioDiagnosticAnalyzerExecutorTests
    {
        [Fact]
        public async Task TestCSharpAnalyzerOptions()
        {
            var code = @"class Test
{
    void Method()
    {
        var t = new Test();
    }
}";

            using var workspace = CreateWorkspace(LanguageNames.CSharp, code);
            var analyzerType = typeof(CSharpUseExplicitTypeDiagnosticAnalyzer);
            var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType,
                IdeAnalyzerOptions.GetDefault(workspace.Services.SolutionServices.GetLanguageServices(LanguageNames.CSharp)));

            var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic);
            Assert.Equal(IDEDiagnosticIds.UseExplicitTypeDiagnosticId, diagnostics[0].Id);
            Assert.Equal(DiagnosticSeverity.Hidden, diagnostics[0].Severity);

            var ideOptions = new IdeAnalyzerOptions()
            {
                CleanCodeGenerationOptions = new()
                {
                    GenerationOptions = CSharpCodeGenerationOptions.Default,
                    CleanupOptions = new()
                    {
                        FormattingOptions = CSharpSyntaxFormattingOptions.Default,
                        SimplifierOptions = new CSharpSimplifierOptions()
                        {
                            VarWhenTypeIsApparent = new CodeStyleOption2<bool>(false, NotificationOption2.Suggestion)
                        }
                    }
                }
            };

            analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, ideOptions);

            diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic);
            Assert.Equal(IDEDiagnosticIds.UseExplicitTypeDiagnosticId, diagnostics[0].Id);
            Assert.Equal(DiagnosticSeverity.Info, diagnostics[0].Severity);
        }

        [Fact]
        public async Task TestVisualBasicAnalyzerOptions()
        {
            var code = @"Class Test
    Sub Method()
        Dim b = Nothing
        Dim a = If(b Is Nothing, Nothing, b.ToString())
    End Sub
End Class";

            using (var workspace = CreateWorkspace(LanguageNames.VisualBasic, code))
            {
                var ideAnalyzerOptions = IdeAnalyzerOptions.GetDefault(workspace.Services.SolutionServices.GetLanguageServices(LanguageNames.VisualBasic));

                ideAnalyzerOptions = ideAnalyzerOptions with
                {
                    CodeStyleOptions = new VisualBasicIdeCodeStyleOptions()
                    {
                        PreferNullPropagation = new CodeStyleOption2<bool>(false, NotificationOption2.Silent)
                    }
                };

                var analyzerType = typeof(VisualBasicUseNullPropagationDiagnosticAnalyzer);
                var analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, ideAnalyzerOptions);

                Assert.True(analyzerResult.IsEmpty);

                ideAnalyzerOptions = ideAnalyzerOptions with
                {
                    CodeStyleOptions = new VisualBasicIdeCodeStyleOptions()
                    {
                        PreferNullPropagation = new CodeStyleOption2<bool>(true, NotificationOption2.Error)
                    }
                };

                analyzerResult = await AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, ideAnalyzerOptions);

                var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic);
                Assert.Equal(IDEDiagnosticIds.UseNullPropagationDiagnosticId, diagnostics[0].Id);
            }
        }

        [Fact]
        public async Task TestCancellation()
        {
            var code = @"class Test { void Method() { } }";

            using var workspace = CreateWorkspace(LanguageNames.CSharp, code);
            var ideAnalyzerOptions = IdeAnalyzerOptions.GetDefault(workspace.Services.SolutionServices.GetLanguageServices(LanguageNames.CSharp));

            var analyzerType = typeof(MyAnalyzer);

            for (var i = 0; i < 5; i++)
            {
                var source = new CancellationTokenSource();

                try
                {
                    var task = Task.Run(() => AnalyzeAsync(workspace, workspace.CurrentSolution.ProjectIds.First(), analyzerType, ideAnalyzerOptions, source.Token));

                    // wait random milli-second
                    var random = new Random(Environment.TickCount);
                    var next = random.Next(1000);
                    await Task.Delay(next);

                    source.Cancel();

                    // let it throw
                    var result = await task;
                }
                catch (Exception ex)
                {
                    // only cancellation is expected
                    Assert.True(ex is OperationCanceledException, $"cancellationToken : {source.Token.IsCancellationRequested}/r/n{ex}");
                }
            }
        }

        [Fact]
        public async Task TestHostAnalyzers_OutOfProc()
        {
            var code = @"class Test
{
    void Method()
    {
        var t = new Test();
    }
}";
            using var workspace = CreateWorkspace(LanguageNames.CSharp, code);
            var analyzerType = typeof(CSharpUseExplicitTypeDiagnosticAnalyzer);

            var analyzerReference = new AnalyzerFileReference(analyzerType.Assembly.Location, new TestAnalyzerAssemblyLoader());
            workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(new[] { analyzerReference }));

            var ideAnalyzerOptions = IdeAnalyzerOptions.GetDefault(workspace.Services.SolutionServices.GetLanguageServices(LanguageNames.CSharp));
            workspace.GlobalOptions.SetGlobalOption(CSharpCodeStyleOptions.VarWhenTypeIsApparent, new CodeStyleOption<bool>(false, NotificationOption.Suggestion));

            // run analysis
            var project = workspace.CurrentSolution.Projects.First();

            var runner = CreateAnalyzerRunner();

            var compilationWithAnalyzers = (await project.GetCompilationAsync()).WithAnalyzers(
                analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(),
                new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions));

            // no result for open file only analyzer unless forced
            var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None);
            Assert.Empty(result.AnalysisResult);

            result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: true, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None);
            var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]];

            // check result
            var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Semantic);
            Assert.Equal(IDEDiagnosticIds.UseExplicitTypeDiagnosticId, diagnostics[0].Id);
        }

        [Fact]
        public async Task TestDuplicatedAnalyzers()
        {
            var code = @"class Test
{
    void Method()
    {
        var t = new Test();
    }
}";

            using var workspace = CreateWorkspace(LanguageNames.CSharp, code);
            var analyzerType = typeof(DuplicateAnalyzer);
            var analyzerReference = new AnalyzerFileReference(analyzerType.Assembly.Location, new TestAnalyzerAssemblyLoader());

            // add host analyzer as global assets
            var remotableDataService = workspace.Services.GetService<ISolutionAssetStorageProvider>();
            var serializer = workspace.Services.GetRequiredService<ISerializerService>();

            // run analysis
            var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(analyzerReference);

            var runner = CreateAnalyzerRunner();
            var analyzers = analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray();

            var ideAnalyzerOptions = IdeAnalyzerOptions.GetDefault(project.Services);

            var compilationWithAnalyzers = (await project.GetCompilationAsync())
                .WithAnalyzers(analyzers, new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions));

            var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false,
                logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None);

            var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]];

            // check result
            var diagnostics = analyzerResult.GetDocumentDiagnostics(analyzerResult.DocumentIds.First(), AnalysisKind.Syntax);
            Assert.Equal("test", diagnostics[0].Id);
        }

        private static InProcOrRemoteHostAnalyzerRunner CreateAnalyzerRunner()
            => new(new DiagnosticAnalyzerInfoCache());

        private static async Task<DiagnosticAnalysisResult> AnalyzeAsync(TestWorkspace workspace, ProjectId projectId, Type analyzerType, IdeAnalyzerOptions ideOptions, CancellationToken cancellationToken = default)
        {
            var executor = CreateAnalyzerRunner();

            var analyzerReference = new AnalyzerFileReference(analyzerType.Assembly.Location, new TestAnalyzerAssemblyLoader());
            var project = workspace.CurrentSolution.GetProject(projectId).AddAnalyzerReference(analyzerReference);

            var analyzerDriver = (await project.GetCompilationAsync()).WithAnalyzers(
                    analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(),
                    new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideOptions));

            var result = await executor.AnalyzeProjectAsync(project, analyzerDriver, forceExecuteAllAnalyzers: true, logPerformanceInfo: false,
                getTelemetryInfo: false, cancellationToken);

            return result.AnalysisResult[analyzerDriver.Analyzers[0]];
        }

        private static TestWorkspace CreateWorkspace(string language, string code, ParseOptions options = null)
        {
            var composition = EditorTestCompositions.EditorFeatures.WithTestHostParts(TestHost.OutOfProcess);

            var workspace = (language == LanguageNames.CSharp)
                ? TestWorkspace.CreateCSharp(code, parseOptions: options, composition: composition)
                : TestWorkspace.CreateVisualBasic(code, parseOptions: options, composition: composition);

            workspace.GlobalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.CSharp, BackgroundAnalysisScope.FullSolution);
            workspace.GlobalOptions.SetGlobalOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.VisualBasic, BackgroundAnalysisScope.FullSolution);

            return workspace;
        }

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        private class MyAnalyzer : DiagnosticAnalyzer
        {
            private readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
                ImmutableArray.Create(new DiagnosticDescriptor("test", "test", "test", "test", DiagnosticSeverity.Error, isEnabledByDefault: true));

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics;

            public override void Initialize(AnalysisContext context)
            {
                context.RegisterSyntaxTreeAction(c =>
                {
                    for (var i = 0; i < 10000; i++)
                    {
                        c.ReportDiagnostic(Diagnostic.Create(_supportedDiagnostics[0], c.Tree.GetLocation(TextSpan.FromBounds(0, 1))));
                    }
                });
            }
        }

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        private class DuplicateAnalyzer : DiagnosticAnalyzer
        {
            private readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
                ImmutableArray.Create(new DiagnosticDescriptor("test", "test", "test", "test", DiagnosticSeverity.Error, isEnabledByDefault: true));

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics;

            public override void Initialize(AnalysisContext context)
            {
                context.RegisterSyntaxTreeAction(c =>
                {
                    c.ReportDiagnostic(Diagnostic.Create(_supportedDiagnostics[0], c.Tree.GetLocation(TextSpan.FromBounds(0, 1))));
                });
            }
        }

        private class MyUpdateSource : AbstractHostDiagnosticUpdateSource
        {
            private readonly Workspace _workspace;

            public MyUpdateSource(Workspace workspace)
            {
                _workspace = workspace;
            }

            public override Workspace Workspace => _workspace;
        }
    }
}
