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

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Classification
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.Classification)]
    public class SyntacticTaggerTests
    {
        [WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1032665")]
        [WpfFact]
        public async Task TestTagsChangedForPortionThatChanged()
        {
            var code =
                """
                class Program2
                {
                    string x = @"/// <summary>$$
                /// </summary>";
                }
                """;
            using var workspace = TestWorkspace.CreateCSharp(code);
            var document = workspace.Documents.First();
            var subjectBuffer = document.GetTextBuffer();

            var checkpoint = new Checkpoint();

            var tagComputer = new SyntacticClassificationTaggerProvider.TagComputer(
                new SyntacticClassificationTaggerProvider(
                    workspace.GetService<IThreadingContext>(),
                    typeMap: null,
                    workspace.GetService<IGlobalOptionService>(),
                    AsynchronousOperationListenerProvider.NullProvider),
                subjectBuffer,
                AsynchronousOperationListenerProvider.NullListener,
                typeMap: null,
                diffTimeout: TimeSpan.MaxValue);

            // Capture the expected value before the await, in case it changes.
            var expectedLength = subjectBuffer.CurrentSnapshot.Length;
            int? actualVersionNumber = null;
            int? actualLength = null;
            var callstacks = new List<string>();
            tagComputer.TagsChanged += (s, e) =>
            {
                actualVersionNumber = e.Span.Snapshot.Version.VersionNumber;
                actualLength = e.Span.Length;
                callstacks.Add(new StackTrace().ToString());
                checkpoint.Release();
            };

            await checkpoint.Task;
            Assert.Equal(0, actualVersionNumber);
            Assert.Equal(expectedLength, actualLength);
            Assert.Equal(1, callstacks.Count);

            checkpoint = new Checkpoint();

            // Now apply an edit that require us to reclassify more that just the current line
            var snapshot = subjectBuffer.Insert(document.CursorPosition.Value, """
                "
                """);
            expectedLength = snapshot.Length;

            // NOTE: TagsChanged is raised on the UI thread, so there is no race between
            // assigning expected here and verifying in the event handler, because the
            // event handler can't run until we await.
            await checkpoint.Task;
            Assert.Equal(1, actualVersionNumber);
            Assert.Equal(37, actualLength);
            Assert.Equal(2, callstacks.Count);
        }

        [WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1032665")]
        [WpfFact]
        public async Task TestTagsChangedAfterDelete()
        {
            var code =
@"class Goo";
            using var workspace = TestWorkspace.CreateCSharp(code);
            var document = workspace.Documents.First();
            var subjectBuffer = document.GetTextBuffer();

            var checkpoint = new Checkpoint();

            var typeMap = workspace.ExportProvider.GetExportedValue<SyntacticClassificationTypeMap>();

            var tagComputer = new SyntacticClassificationTaggerProvider.TagComputer(
                new SyntacticClassificationTaggerProvider(
                    workspace.GetService<IThreadingContext>(),
                    typeMap,
                    workspace.GetService<IGlobalOptionService>(),
                    AsynchronousOperationListenerProvider.NullProvider),
                subjectBuffer,
                AsynchronousOperationListenerProvider.NullListener,
                typeMap,
                diffTimeout: TimeSpan.MaxValue);

            // Capture the expected value before the await, in case it changes.
            var expectedLength = subjectBuffer.CurrentSnapshot.Length;
            int? actualVersionNumber = null;
            int? actualLength = null;
            var callstacks = new List<string>();
            tagComputer.TagsChanged += (s, e) =>
            {
                actualVersionNumber = e.Span.Snapshot.Version.VersionNumber;
                actualLength = e.Span.Length;
                callstacks.Add(new StackTrace().ToString());
                checkpoint.Release();
            };

            await checkpoint.Task;
            Assert.Equal(0, actualVersionNumber);
            Assert.Equal(expectedLength, actualLength);
            Assert.Equal(1, callstacks.Count);

            checkpoint = new Checkpoint();

            // Now delete the last character.
            var snapshot = subjectBuffer.Delete(new Span(subjectBuffer.CurrentSnapshot.Length - 1, 1));

            // Try to get the tags prior to TagsChanged firing.  This will force us to use the previous 
            // data we've cached to produce the new results.
            tagComputer.GetTags(new NormalizedSnapshotSpanCollection(subjectBuffer.CurrentSnapshot.GetFullSpan()));

            expectedLength = snapshot.Length;

            // NOTE: TagsChanged is raised on the UI thread, so there is no race between
            // assigning expected here and verifying in the event handler, because the
            // event handler can't run until we await.
            await checkpoint.Task;

            Assert.Equal(1, actualVersionNumber);
            Assert.Equal(2, actualLength);
            Assert.Equal(2, callstacks.Count);
        }
    }
}
