﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Implementation.Structure;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Tagging
{
    [UseExportProvider]
    public class AsynchronousTaggerTests : TestBase
    {
        /// <summary>
        /// This hits a special codepath in the product that is optimized for more than 100 spans.
        /// I'm leaving this test here because it covers that code path (as shown by code coverage)
        /// </summary>
        [WpfFact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/530368")]
        public async Task LargeNumberOfSpans()
        {
            using var workspace = TestWorkspace.CreateCSharp(@"class Program
{
    void M()
    {
        int z = 0;
        z = z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z +
            z + z + z + z + z + z + z + z + z + z;
    }
}");
            static List<ITagSpan<TestTag>> tagProducer(SnapshotSpan span, CancellationToken cancellationToken)
            {
                return new List<ITagSpan<TestTag>>() { new TagSpan<TestTag>(span, new TestTag()) };
            }

            var asyncListener = new AsynchronousOperationListener();

            WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(LargeNumberOfSpans)} creates asynchronous taggers");

            var eventSource = CreateEventSource();
            var taggerProvider = new TestTaggerProvider(
                workspace.GetService<IThreadingContext>(),
                tagProducer,
                eventSource,
                workspace.GetService<IGlobalOptionService>(),
                asyncListener);

            var document = workspace.Documents.First();
            var textBuffer = document.GetTextBuffer();
            var snapshot = textBuffer.CurrentSnapshot;
            var tagger = taggerProvider.CreateTagger<TestTag>(textBuffer);
            Contract.ThrowIfNull(tagger);

            using var disposable = (IDisposable)tagger;
            var spans = Enumerable.Range(0, 101).Select(i => new Span(i * 4, 1));
            var snapshotSpans = new NormalizedSnapshotSpanCollection(snapshot, spans);

            eventSource.SendUpdateEvent();

            await asyncListener.ExpeditedWaitAsync();

            var tags = tagger.GetTags(snapshotSpans);

            Assert.Equal(1, tags.Count());
        }

        [WpfFact]
        public void TestNotSynchronousOutlining()
        {
            using var workspace = TestWorkspace.CreateCSharp("class Program {\r\n\r\n}", composition: EditorTestCompositions.EditorFeaturesWpf);
            WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestNotSynchronousOutlining)} creates asynchronous taggers");

            var tagProvider = workspace.ExportProvider.GetExportedValue<AbstractStructureTaggerProvider>();

            var document = workspace.Documents.First();
            var textBuffer = document.GetTextBuffer();
            var tagger = tagProvider.CreateTagger<IStructureTag>(textBuffer);
            Contract.ThrowIfNull(tagger);

            using var disposable = (IDisposable)tagger;
            // The very first all to get tags will not be synchronous as this contains no #region tag
            var tags = tagger.GetTags(new NormalizedSnapshotSpanCollection(textBuffer.CurrentSnapshot.GetFullSpan()));
            Assert.Equal(0, tags.Count());
        }

        [WpfFact]
        public void TestSynchronousOutlining()
        {
            using var workspace = TestWorkspace.CreateCSharp(@"
#region x

class Program
{
}

#endregion", composition: EditorTestCompositions.EditorFeaturesWpf);
            WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestSynchronousOutlining)} creates asynchronous taggers");

            var tagProvider = workspace.ExportProvider.GetExportedValue<AbstractStructureTaggerProvider>();

            var document = workspace.Documents.First();
            var textBuffer = document.GetTextBuffer();
            var tagger = tagProvider.CreateTagger<IStructureTag>(textBuffer);
            Contract.ThrowIfNull(tagger);

            using var disposable = (IDisposable)tagger;
            // The very first all to get tags will be synchronous because of the #region
            var tags = tagger.GetTags(new NormalizedSnapshotSpanCollection(textBuffer.CurrentSnapshot.GetFullSpan()));
            Assert.Equal(2, tags.Count());
        }

        private static TestTaggerEventSource CreateEventSource()
            => new TestTaggerEventSource();

        private sealed class TestTag : TextMarkerTag
        {
            public TestTag()
                : base("Test")
            {
            }
        }

        private delegate List<ITagSpan<TestTag>> Callback(SnapshotSpan span, CancellationToken cancellationToken);

        private sealed class TestTaggerProvider : AsynchronousTaggerProvider<TestTag>
        {
            private readonly Callback _callback;
            private readonly ITaggerEventSource _eventSource;

            public TestTaggerProvider(
                IThreadingContext threadingContext,
                Callback callback,
                ITaggerEventSource eventSource,
                IGlobalOptionService globalOptions,
                IAsynchronousOperationListener asyncListener)
                : base(threadingContext, globalOptions, visibilityTracker: null, asyncListener)
            {
                _callback = callback;
                _eventSource = eventSource;
            }

            protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate;

            protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer)
                => _eventSource;

            protected override Task ProduceTagsAsync(
                TaggerContext<TestTag> context, DocumentSnapshotSpan snapshotSpan, int? caretPosition, CancellationToken cancellationToken)
            {
                var tags = _callback(snapshotSpan.SnapshotSpan, cancellationToken);
                if (tags != null)
                {
                    foreach (var tag in tags)
                    {
                        context.AddTag(tag);
                    }
                }

                return Task.CompletedTask;
            }

            protected override bool TagEquals(TestTag tag1, TestTag tag2)
                => tag1 == tag2;
        }

        private sealed class TestTaggerEventSource : AbstractTaggerEventSource
        {
            public TestTaggerEventSource()
            {
            }

            public void SendUpdateEvent()
                => this.RaiseChanged();

            public override void Connect()
            {
            }

            public override void Disconnect()
            {
            }
        }
    }
}
