﻿// 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.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Squiggles;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.Diagnostics), Trait(Traits.Feature, Traits.Features.Tagging)]
    public class DiagnosticsClassificationTaggerProviderTests
    {
        [WpfTheory, CombinatorialData]
        public async Task Test_FadingSpans(bool throughAdditionalLocations)
        {
            var analyzer = new Analyzer(diagnosticId: "test", throughAdditionalLocations);
            var analyzerMap = new Dictionary<string, ImmutableArray<DiagnosticAnalyzer>>
            {
                {  LanguageNames.CSharp, ImmutableArray.Create<DiagnosticAnalyzer>(analyzer) }
            };

            using var workspace = TestWorkspace.CreateCSharp(new string[] { "class A { }", "class E { }" }, parseOptions: CSharpParseOptions.Default, composition: SquiggleUtilities.CompositionWithSolutionCrawler);
            using var wrapper = new DiagnosticTaggerWrapper<DiagnosticsClassificationTaggerProvider, ClassificationTag>(workspace, analyzerMap);

            var firstDocument = workspace.Documents.First();
            var tagger = wrapper.TaggerProvider.CreateTagger<ClassificationTag>(firstDocument.GetTextBuffer());
            using var disposable = tagger as IDisposable;
            // test first update
            await wrapper.WaitForTags();

            var snapshot = firstDocument.GetTextBuffer().CurrentSnapshot;
            var spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
            if (!throughAdditionalLocations)
            {
                // We should get a single tag span, which is diagnostic's primary location.
                Assert.Equal(1, spans.Count);

                Assert.Equal(new Span(0, 10), spans[0].Span.Span);

                Assert.Equal(ClassificationTypeDefinitions.UnnecessaryCode, spans[0].Tag.ClassificationType.Classification);
            }
            else
            {
                // We should get two spans, the 1-index and 2-index additional locations in the original diagnostic.
                Assert.Equal(2, spans.Count);

                Assert.Equal(new Span(0, 1), spans[0].Span.Span);
                Assert.Equal(new Span(9, 1), spans[1].Span.Span);

                Assert.Equal(ClassificationTypeDefinitions.UnnecessaryCode, spans[0].Tag.ClassificationType.Classification);
                Assert.Equal(ClassificationTypeDefinitions.UnnecessaryCode, spans[1].Tag.ClassificationType.Classification);
            }
        }

        private class Analyzer : DiagnosticAnalyzer
        {
            private readonly bool _throughAdditionalLocations;
            private readonly DiagnosticDescriptor _rule;

            public Analyzer(string diagnosticId, bool throughAdditionalLocations)
            {
                _throughAdditionalLocations = throughAdditionalLocations;
                _rule = new(
                    diagnosticId, "test", "test", "test", DiagnosticSeverity.Error, true,
                    customTags: DiagnosticCustomTags.Create(isUnnecessary: true, isConfigurable: false, EnforceOnBuild.Never));
            }

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
                => ImmutableArray.Create(_rule);

            public override void Initialize(AnalysisContext context)
            {
                context.RegisterSyntaxTreeAction(c =>
                {
                    var primaryLocation = Location.Create(c.Tree, new TextSpan(0, 10));
                    if (!_throughAdditionalLocations)
                    {
                        c.ReportDiagnostic(DiagnosticHelper.Create(
                            _rule, primaryLocation,
                            ReportDiagnostic.Error,
                            additionalLocations: null,
                            properties: null));
                    }
                    else
                    {
                        var additionalLocations = ImmutableArray.Create(Location.Create(c.Tree, new TextSpan(0, 10)));
                        var additionalUnnecessaryLocations = ImmutableArray.Create(
                            Location.Create(c.Tree, new TextSpan(0, 1)),
                            Location.Create(c.Tree, new TextSpan(9, 1)));

                        c.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
                            _rule, primaryLocation,
                            ReportDiagnostic.Error,
                            additionalLocations,
                            additionalUnnecessaryLocations));
                    }
                });
            }
        }

        [WpfTheory, WorkItem("https://github.com/dotnet/roslyn/issues/62183")]
        [InlineData(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, true)]
        [InlineData(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, false)]
        [InlineData(IDEDiagnosticIds.RemoveUnreachableCodeDiagnosticId, true)]
        [InlineData(IDEDiagnosticIds.RemoveUnreachableCodeDiagnosticId, false)]
        public async Task Test_FadingOptions(string diagnosticId, bool fadingOptionValue)
        {
            var analyzer = new Analyzer(diagnosticId, throughAdditionalLocations: false);
            var analyzerMap = new Dictionary<string, ImmutableArray<DiagnosticAnalyzer>>
            {
                {  LanguageNames.CSharp, ImmutableArray.Create<DiagnosticAnalyzer>(analyzer) }
            };

            using var workspace = TestWorkspace.CreateCSharp(new string[] { "class A { }", "class E { }" }, parseOptions: CSharpParseOptions.Default, composition: SquiggleUtilities.CompositionWithSolutionCrawler);

            // Set fading option
            var fadingOption = GetFadingOptionForDiagnostic(diagnosticId);
            workspace.GlobalOptions.SetGlobalOption(fadingOption, LanguageNames.CSharp, fadingOptionValue);

            // Add mapping from diagnostic ID to fading option
            IDEDiagnosticIdToOptionMappingHelper.AddFadingOptionMapping(diagnosticId, fadingOption);

            // Set up the tagger
            using var wrapper = new DiagnosticTaggerWrapper<DiagnosticsClassificationTaggerProvider, ClassificationTag>(workspace, analyzerMap);

            var firstDocument = workspace.Documents.First();
            var tagger = wrapper.TaggerProvider.CreateTagger<ClassificationTag>(firstDocument.GetTextBuffer());
            using var disposable = tagger as IDisposable;
            // test first update
            await wrapper.WaitForTags();

            var snapshot = firstDocument.GetTextBuffer().CurrentSnapshot;
            var spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToList();
            if (!fadingOptionValue)
            {
                // We should get no tag spans when the fading option is disabled.
                Assert.Empty(spans);
            }
            else
            {
                // We should get a single tag span, which is diagnostic's primary location.
                Assert.Equal(1, spans.Count);

                Assert.Equal(new Span(0, 10), spans[0].Span.Span);

                Assert.Equal(ClassificationTypeDefinitions.UnnecessaryCode, spans[0].Tag.ClassificationType.Classification);
            }
        }

        private static PerLanguageOption2<bool> GetFadingOptionForDiagnostic(string diagnosticId)
            => diagnosticId switch
            {
                IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId => FadingOptions.FadeOutUnusedImports,
                IDEDiagnosticIds.RemoveUnreachableCodeDiagnosticId => FadingOptions.FadeOutUnreachableCode,
                _ => throw ExceptionUtilities.UnexpectedValue(diagnosticId),
            };
    }
}
