﻿// 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.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.HighlightTags;
using Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.GoToBase;
using Microsoft.CodeAnalysis.GoToImplementation;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.LanguageServices.FindUsages;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using Roslyn.VisualStudio.IntegrationTests;
using Roslyn.VisualStudio.IntegrationTests.InProcess;
using WindowsInput.Native;
using Xunit;
using IComponentModel = Microsoft.VisualStudio.ComponentModelHost.IComponentModel;
using IObjectWithSite = Microsoft.VisualStudio.OLE.Interop.IObjectWithSite;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
using IPersistFile = Microsoft.VisualStudio.OLE.Interop.IPersistFile;
using OLECMDEXECOPT = Microsoft.VisualStudio.OLE.Interop.OLECMDEXECOPT;
using SComponentModel = Microsoft.VisualStudio.ComponentModelHost.SComponentModel;
using TextSpan = Microsoft.CodeAnalysis.Text.TextSpan;

namespace Microsoft.VisualStudio.Extensibility.Testing
{
    internal partial class EditorInProcess
    {
        public async Task WaitForEditorOperationsAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var shell = await GetRequiredGlobalServiceAsync<SVsShell, IVsShell>(cancellationToken);
            if (shell.IsPackageLoaded(DefGuidList.guidEditorPkg, out var editorPackage) == VSConstants.S_OK)
            {
                var asyncPackage = (AsyncPackage)editorPackage;
                var collection = asyncPackage.GetPropertyValue<JoinableTaskCollection>("JoinableTaskCollection");
                await collection.JoinTillEmptyAsync(cancellationToken);
            }
        }

        public async Task<bool> IsSavedAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var vsView = await GetActiveVsTextViewAsync(cancellationToken);
            ErrorHandler.ThrowOnFailure(vsView.GetBuffer(out var buffer));

            // From CVsDocument::get_Saved
            if (buffer is IVsPersistDocData persistDocData)
            {
                ErrorHandler.ThrowOnFailure(persistDocData.IsDocDataDirty(out var dirty));
                return dirty == 0;
            }
            else if (buffer is IPersistFile persistFile)
            {
                return persistFile.IsDirty() == 0;
            }
            else if (buffer is IPersistFileFormat persistFileFormat)
            {
                ErrorHandler.ThrowOnFailure(persistFileFormat.IsDirty(out var dirty));
                return dirty == 0;
            }
            else
            {
                throw new InvalidOperationException("Unsupported document");
            }
        }

        public async Task<Document?> GetActiveDocumentAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            return view.TextBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        }

        public async Task SetTextAsync(string text, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var textSnapshot = view.TextSnapshot;
            var replacementSpan = new SnapshotSpan(textSnapshot, 0, textSnapshot.Length);
            view.TextBuffer.Replace(replacementSpan, text);
        }

        public async Task<string> GetTextAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var bufferPosition = view.Caret.Position.BufferPosition;
            return bufferPosition.Snapshot.GetText();
        }

        public async Task<string> GetCurrentLineTextAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var bufferPosition = view.Caret.Position.BufferPosition;
            var line = bufferPosition.GetContainingLine();
            return line.GetText();
        }

        public async Task<string> GetSelectedTextAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var subjectBuffer = view.GetBufferContainingCaret();
            Contract.ThrowIfNull(subjectBuffer);

            var selectedSpan = view.Selection.SelectedSpans[0];
            return subjectBuffer.CurrentSnapshot.GetText(selectedSpan);
        }

        public async Task<string> GetLineTextBeforeCaretAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var bufferPosition = view.Caret.Position.BufferPosition;
            var line = bufferPosition.GetContainingLine();
            var lineText = line.GetText();
            var lineTextBeforeCaret = lineText[..(bufferPosition.Position - line.Start)];
            return lineTextBeforeCaret;
        }

        public async Task<string> GetLineTextAfterCaretAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var bufferPosition = view.Caret.Position.BufferPosition;
            var line = bufferPosition.GetContainingLine();
            var lineText = line.GetText();
            var lineTextAfterCaret = lineText[(bufferPosition.Position - line.Start)..];
            return lineTextAfterCaret;
        }

        public async Task MoveCaretAsync(int position, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var point = new SnapshotPoint(view.TextSnapshot, position);

            view.Caret.MoveTo(point);
        }

        public async Task SetMultiSelectionAsync(ImmutableArray<TextSpan> positions, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var subjectBuffer = view.GetBufferContainingCaret();
            Assumes.Present(subjectBuffer);

            view.SetMultiSelection(positions.Select(p => new SnapshotSpan(subjectBuffer.CurrentSnapshot, p.Start, p.Length)));
        }

        public async Task SelectTextInCurrentDocumentAsync(string text, CancellationToken cancellationToken)
        {
            await PlaceCaretAsync(text, charsOffset: -1, occurrence: 0, extendSelection: false, selectBlock: false, cancellationToken);
            await PlaceCaretAsync(text, charsOffset: 0, occurrence: 0, extendSelection: true, selectBlock: false, cancellationToken);
        }

        public async Task DeleteTextAsync(string text, CancellationToken cancellationToken)
        {
            await SelectTextInCurrentDocumentAsync(text, cancellationToken);
            await TestServices.Input.SendAsync(VirtualKeyCode.DELETE, cancellationToken);
        }

        public async Task PasteAsync(string text, CancellationToken cancellationToken)
        {
            var provider = await TestServices.Shell.GetComponentModelServiceAsync<IAsynchronousOperationListenerProvider>(cancellationToken);
            var waiter = (IAsynchronousOperationWaiter)provider.GetListener(FeatureAttribute.AddImportsOnPaste);

            await TestServices.Workspace.WaitForAllAsyncOperationsAsync(new[] { FeatureAttribute.Workspace, FeatureAttribute.SolutionCrawlerLegacy }, cancellationToken);
            Clipboard.SetText(text);
            await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd97CmdID.Paste, cancellationToken);

            await waiter.ExpeditedWaitAsync();
        }

        public async Task<ClassificationSpan[]> GetLightBulbPreviewClassificationsAsync(string menuText, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
            var classifierAggregatorService = await GetComponentModelServiceAsync<IViewClassifierAggregatorService>(cancellationToken);

            await LightBulbHelper.WaitForLightBulbSessionAsync(TestServices, broker, view, cancellationToken).ConfigureAwait(true);

            var bufferType = view.TextBuffer.ContentType.DisplayName;
            if (!broker.IsLightBulbSessionActive(view))
            {
                throw new Exception($"No Active Smart Tags in View!  Buffer content type='{bufferType}'");
            }

            var activeSession = broker.GetSession(view);
            if (activeSession == null || !activeSession.IsExpanded)
            {
                throw new InvalidOperationException($"No expanded light bulb session found after View.ShowSmartTag.  Buffer content type='{bufferType}'");
            }

            if (!string.IsNullOrEmpty(menuText))
            {
#pragma warning disable CS0618 // Type or member is obsolete
                if (activeSession.TryGetSuggestedActionSets(out var actionSets) != QuerySuggestedActionCompletionStatus.Completed)
                {
                    actionSets = Array.Empty<SuggestedActionSet>();
                }
#pragma warning restore CS0618 // Type or member is obsolete

                var set = actionSets.SelectMany(s => s.Actions).FirstOrDefault(a => a.DisplayText == menuText);
                if (set == null)
                {
                    throw new InvalidOperationException(
                        $"ISuggestionAction '{menuText}' not found.  Buffer content type='{bufferType}'");
                }

                IWpfTextView? preview = null;
                var pane = await set.GetPreviewAsync(CancellationToken.None).ConfigureAwait(true);
                if (pane is UserControl control)
                {
                    var container = control.FindName("PreviewDockPanel") as DockPanel;
                    var host = container.FindDescendants<UIElement>().OfType<IWpfTextViewHost>().LastOrDefault();
                    preview = host?.TextView;
                }

                if (preview == null)
                {
                    throw new InvalidOperationException(string.Format("Could not find light bulb preview.  Buffer content type={0}", bufferType));
                }

                activeSession.Collapse();
                var classifier = classifierAggregatorService.GetClassifier(preview);
                var classifiedSpans = classifier.GetClassificationSpans(new SnapshotSpan(preview.TextBuffer.CurrentSnapshot, 0, preview.TextBuffer.CurrentSnapshot.Length));
                return classifiedSpans.ToArray();
            }

            activeSession.Collapse();
            return Array.Empty<ClassificationSpan>();
        }

        public async Task InvokeQuickInfoAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var broker = await TestServices.Shell.GetComponentModelServiceAsync<IAsyncQuickInfoBroker>(cancellationToken);
            var session = await broker.TriggerQuickInfoAsync(await TestServices.Editor.GetActiveTextViewAsync(cancellationToken), cancellationToken: cancellationToken);
            Contract.ThrowIfNull(session);
        }

        public async Task<string> GetQuickInfoAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
            var broker = await TestServices.Shell.GetComponentModelServiceAsync<IAsyncQuickInfoBroker>(cancellationToken);

            var session = broker.GetSession(view);

            // GetSession will not return null if preceded by a call to InvokeQuickInfo
            Contract.ThrowIfNull(session);

            while (session.State != QuickInfoSessionState.Visible)
            {
                cancellationToken.ThrowIfCancellationRequested();
                await Task.Delay(50, cancellationToken).ConfigureAwait(true);
            }

            return QuickInfoToStringConverter.GetStringFromBulkContent(session.Content);
        }

        public async Task<string[]> GetCurrentClassificationsAsync(CancellationToken cancellationToken)
        {
            IClassifier? classifier = null;
            try
            {
                var textView = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
                var selectionSpan = textView.Selection.StreamSelectionSpan.SnapshotSpan;
                if (selectionSpan.Length == 0)
                {
                    var textStructureNavigatorSelectorService = await TestServices.Shell.GetComponentModelServiceAsync<ITextStructureNavigatorSelectorService>(cancellationToken);
                    selectionSpan = textStructureNavigatorSelectorService
                        .GetTextStructureNavigator(textView.TextBuffer)
                        .GetExtentOfWord(selectionSpan.Start).Span;
                }

                var classifierAggregatorService = await TestServices.Shell.GetComponentModelServiceAsync<IViewClassifierAggregatorService>(cancellationToken);
                classifier = classifierAggregatorService.GetClassifier(textView);
                var classifiedSpans = classifier.GetClassificationSpans(selectionSpan);
                return classifiedSpans.Select(x => x.ClassificationType.Classification).ToArray();
            }
            finally
            {
                if (classifier is IDisposable classifierDispose)
                {
                    classifierDispose.Dispose();
                }
            }
        }

        public async Task ActivateAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var dte = await GetRequiredGlobalServiceAsync<SDTE, EnvDTE.DTE>(cancellationToken);
            dte.ActiveDocument.Activate();
        }

        public async Task SendExplicitFocusAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var textView = await GetActiveVsTextViewAsync(cancellationToken);
            textView.SendExplicitFocus();
        }

        public async Task<bool> IsUseSuggestionModeOnAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var textView = await GetActiveTextViewAsync(cancellationToken);
            return await IsUseSuggestionModeOnAsync(forDebuggerTextView: IsDebuggerTextView(textView), cancellationToken);
        }

        public async Task<bool> IsUseSuggestionModeOnAsync(bool forDebuggerTextView, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var editorOptionsFactory = await GetComponentModelServiceAsync<IEditorOptionsFactoryService>(cancellationToken);
            var options = editorOptionsFactory.GlobalOptions;

            EditorOptionKey<bool> optionKey;
            bool defaultOption;
            if (forDebuggerTextView)
            {
                optionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInDebuggerCompletionOptionName);
                defaultOption = true;
            }
            else
            {
                optionKey = new EditorOptionKey<bool>(PredefinedCompletionNames.SuggestionModeInCompletionOptionName);
                defaultOption = false;
            }

            if (!options.IsOptionDefined(optionKey, localScopeOnly: false))
            {
                return defaultOption;
            }

            return options.GetOptionValue(optionKey);
        }

        public async Task SetUseSuggestionModeAsync(bool value, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var textView = await GetActiveTextViewAsync(cancellationToken);
            await SetUseSuggestionModeAsync(forDebuggerTextView: IsDebuggerTextView(textView), value, cancellationToken);
        }

        public async Task SetUseSuggestionModeAsync(bool forDebuggerTextView, bool value, CancellationToken cancellationToken)
        {
            if (await IsUseSuggestionModeOnAsync(forDebuggerTextView, cancellationToken) != value)
            {
                await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd2KCmdID.ToggleConsumeFirstCompletionMode, cancellationToken);
                if (await IsUseSuggestionModeOnAsync(forDebuggerTextView, cancellationToken) != value)
                {
                    throw new InvalidOperationException($"{WellKnownCommandNames.Edit_ToggleCompletionMode} did not leave the editor in the expected state.");
                }
            }
        }

        public Task<ImmutableArray<TagSpan<IErrorTag>>> GetErrorTagsAsync(CancellationToken cancellationToken)
        {
            return GetTagsAsync<IErrorTag>(cancellationToken);
        }

        public async Task<ImmutableArray<TagSpan<ITextMarkerTag>>> GetRenameTagsAsync(CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForRenameAsync(cancellationToken);
            var tags = await GetTagsAsync<ITextMarkerTag>(cancellationToken);
            return tags.WhereAsArray(tag => tag.Tag.Type == RenameFieldBackgroundAndBorderTag.TagId);
        }

        public async Task<ImmutableArray<TagSpan<TTag>>> GetTagsAsync<TTag>(CancellationToken cancellationToken)
            where TTag : ITag
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            var view = await GetActiveTextViewAsync(cancellationToken);
            var viewTagAggregatorFactory = await GetComponentModelServiceAsync<IViewTagAggregatorFactoryService>(cancellationToken);

            var aggregator = viewTagAggregatorFactory.CreateTagAggregator<TTag>(view);
            var tags = aggregator
                .GetTags(new SnapshotSpan(view.TextSnapshot, 0, view.TextSnapshot.Length))
                .Cast<IMappingTagSpan<ITag>>();

            return tags.SelectAsArray(tag => (new TagSpan<TTag>(tag.Span.GetSpans(view.TextBuffer).Single(), (TTag)tag.Tag)));
        }

        private static bool IsDebuggerTextView(ITextView textView)
            => textView.Roles.Contains("DEBUGVIEW");

        public async Task<ImmutableArray<string>> GetF1KeywordsAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var vsView = await GetActiveVsTextViewAsync(cancellationToken);
            ErrorHandler.ThrowOnFailure(vsView.GetBuffer(out var textLines));
            ErrorHandler.ThrowOnFailure(textLines.GetLanguageServiceID(out var languageServiceGuid));

            var languageService = await ((AsyncServiceProvider)AsyncServiceProvider.GlobalProvider).QueryServiceAsync(languageServiceGuid).WithCancellation(cancellationToken);
            Assumes.Present(languageService);

            var languageContextProvider = (IVsLanguageContextProvider)languageService;
            var monitorUserContext = await GetRequiredGlobalServiceAsync<SVsMonitorUserContext, IVsMonitorUserContext>(cancellationToken);
            ErrorHandler.ThrowOnFailure(monitorUserContext.CreateEmptyContext(out var emptyUserContext));
            ErrorHandler.ThrowOnFailure(vsView.GetCaretPos(out var line, out var column));

            var span = new TextManager.Interop.TextSpan()
            {
                iStartLine = line,
                iStartIndex = column,
                iEndLine = line,
                iEndIndex = column,
            };

            ErrorHandler.ThrowOnFailure(languageContextProvider.UpdateLanguageContext(dwHint: 0, textLines, new[] { span }, emptyUserContext));
            ErrorHandler.ThrowOnFailure(emptyUserContext.CountAttributes("keyword", fIncludeChildren: Convert.ToInt32(true), out var count));
            var results = ImmutableArray.CreateBuilder<string>(count);
            for (var i = 0; i < count; i++)
            {
                emptyUserContext.GetAttribute(i, "keyword", fIncludeChildren: Convert.ToInt32(true), pbstrName: out _, out var value);
                results.Add(value);
            }

            return results.MoveToImmutable();
        }

        #region Navigation bars

        public async Task ExpandNavigationBarAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
            FocusManager.SetFocusedElement(FocusManager.GetFocusScope(combobox), combobox);
            combobox.IsDropDownOpen = true;
        }

        public async Task<ImmutableArray<string>> GetNavigationBarItemsAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
            return combobox.Items.OfType<object>().SelectAsArray(i => $"{i}");
        }

        public async Task<string?> GetNavigationBarSelectionAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
            return combobox.SelectedItem?.ToString();
        }

        public async Task SelectNavigationBarItemAsync(NavigationBarDropdownKind index, string item, CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var itemIndex = await GetNavigationBarItemIndexAsync(index, item, cancellationToken);
            if (itemIndex < 0)
            {
                Assert.Contains(item, await GetNavigationBarItemsAsync(index, cancellationToken));
                throw ExceptionUtilities.Unreachable();
            }

            await ExpandNavigationBarAsync(index, cancellationToken);
            await TestServices.Input.SendAsync(VirtualKeyCode.HOME, cancellationToken);
            for (var i = 0; i < itemIndex; i++)
            {
                await TestServices.Input.SendAsync(VirtualKeyCode.DOWN, cancellationToken);
            }

            await TestServices.Input.SendAsync(VirtualKeyCode.RETURN, cancellationToken);

            // Navigation and/or code generation following selection is tracked under FeatureAttribute.NavigationBar
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);
        }

        public async Task<int> GetNavigationBarItemIndexAsync(NavigationBarDropdownKind index, string item, CancellationToken cancellationToken)
        {
            var items = await GetNavigationBarItemsAsync(index, cancellationToken);
            return items.IndexOf(item);
        }

        public async Task<bool> IsNavigationBarEnabledAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            return (await GetNavigationBarMarginAsync(view, cancellationToken)) is not null;
        }

        private async Task<List<ComboBox>> GetNavigationBarComboBoxesAsync(IWpfTextView textView, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var margin = await GetNavigationBarMarginAsync(textView, cancellationToken);
            return margin.GetFieldValue<List<ComboBox>>("_combos");
        }

        private async Task<UIElement?> GetNavigationBarMarginAsync(IWpfTextView textView, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var editorAdaptersFactoryService = await GetComponentModelServiceAsync<IVsEditorAdaptersFactoryService>(cancellationToken);
            var viewAdapter = editorAdaptersFactoryService.GetViewAdapter(textView);
            Assumes.Present(viewAdapter);

            // Make sure we have the top pane
            //
            // The docs are wrong. When a secondary view exists, it is the secondary view which is on top. The primary
            // view is only on top when there is no secondary view.
            var codeWindow = TryGetCodeWindow(viewAdapter);
            Assumes.Present(codeWindow);

            if (ErrorHandler.Succeeded(codeWindow.GetSecondaryView(out var secondaryViewAdapter)))
            {
                viewAdapter = secondaryViewAdapter;
            }

            var textViewHost = editorAdaptersFactoryService.GetWpfTextViewHost(viewAdapter);
            Assumes.Present(textViewHost);

            var dropDownMargin = textViewHost.GetTextViewMargin("DropDownMargin");
            if (dropDownMargin != null)
            {
                return ((Decorator)dropDownMargin.VisualElement).Child;
            }

            return null;

            static IVsCodeWindow? TryGetCodeWindow(IVsTextView textView)
            {
                if (textView is not IObjectWithSite objectWithSite)
                {
                    return null;
                }

                var riid = typeof(IOleServiceProvider).GUID;
                objectWithSite.GetSite(ref riid, out var ppvSite);
                if (ppvSite == IntPtr.Zero)
                {
                    return null;
                }

                IOleServiceProvider? oleServiceProvider = null;
                try
                {
                    oleServiceProvider = Marshal.GetObjectForIUnknown(ppvSite) as IOleServiceProvider;
                }
                finally
                {
                    Marshal.Release(ppvSite);
                }

                if (oleServiceProvider == null)
                {
                    return null;
                }

                var guidService = typeof(SVsWindowFrame).GUID;
                riid = typeof(IVsWindowFrame).GUID;
                if (ErrorHandler.Failed(oleServiceProvider.QueryService(ref guidService, ref riid, out var ppvObject)) || ppvObject == IntPtr.Zero)
                {
                    return null;
                }

                IVsWindowFrame? frame = null;
                try
                {
                    frame = (IVsWindowFrame)Marshal.GetObjectForIUnknown(ppvObject);
                }
                finally
                {
                    Marshal.Release(ppvObject);
                }

                riid = typeof(IVsCodeWindow).GUID;
                if (ErrorHandler.Failed(frame.QueryViewInterface(ref riid, out ppvObject)) || ppvObject == IntPtr.Zero)
                {
                    return null;
                }

                try
                {
                    return Marshal.GetObjectForIUnknown(ppvObject) as IVsCodeWindow;
                }
                finally
                {
                    Marshal.Release(ppvObject);
                }
            }
        }

        #endregion

        public async Task DismissLightBulbSessionAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
            broker.DismissSession(view);
        }

        public async Task DismissCompletionSessionsAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            await WaitForCompletionSetAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var broker = await GetComponentModelServiceAsync<ICompletionBroker>(cancellationToken);
            broker.DismissAllSessions(view);
        }

        public async Task<bool> IsCompletionActiveAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            await WaitForCompletionSetAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);
            if (view is null)
                return false;

            var broker = await TestServices.Shell.GetComponentModelServiceAsync<ICompletionBroker>(cancellationToken);
            return broker.IsCompletionActive(view);
        }

        public async Task ShowLightBulbAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var shell = await GetRequiredGlobalServiceAsync<SVsUIShell, IVsUIShell>(cancellationToken);
            var cmdGroup = typeof(VSConstants.VSStd14CmdID).GUID;
            var cmdExecOpt = OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;

            var cmdID = VSConstants.VSStd14CmdID.ShowQuickFixes;
            object? obj = null;
            shell.PostExecCommand(cmdGroup, (uint)cmdID, (uint)cmdExecOpt, ref obj);

            var view = await GetActiveTextViewAsync(cancellationToken);
            var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
            await LightBulbHelper.WaitForLightBulbSessionAsync(TestServices, broker, view, cancellationToken);
        }

        public async Task InvokeCodeActionListAsync(CancellationToken cancellationToken)
        {
            await TestServices.Workarounds.WaitForLightBulbAsync(cancellationToken);

            await InvokeCodeActionListWithoutWaitingAsync(cancellationToken);

            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.LightBulb, cancellationToken);
        }

        public async Task InvokeCodeActionListWithoutWaitingAsync(CancellationToken cancellationToken)
        {
            if (Version.Parse("17.2.32210.308") > await TestServices.Shell.GetVersionAsync(cancellationToken))
            {
                // Workaround for extremely unstable async lightbulb (can dismiss itself when SuggestedActionsChanged
                // fires while expanding the light bulb).
                await TestServices.Input.SendAsync((VirtualKeyCode.OEM_PERIOD, VirtualKeyCode.CONTROL), cancellationToken);
                await Task.Delay(5000, cancellationToken);

                await TestServices.Editor.DismissLightBulbSessionAsync(cancellationToken);
                await Task.Delay(5000, cancellationToken);
            }

            await ShowLightBulbAsync(cancellationToken);
        }

        public async Task<bool> IsLightBulbSessionExpandedAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
            if (!broker.IsLightBulbSessionActive(view))
            {
                return false;
            }

            var session = broker.GetSession(view);
            if (session == null || !session.IsExpanded)
            {
                return false;
            }

            return true;
        }

        public async Task<string[]> GetLightBulbActionsAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);
            return (await GetLightBulbActionsAsync(broker, view, cancellationToken)).Select(a => a.DisplayText).ToArray();
        }

        public async Task<bool> ApplyLightBulbActionAsync(string actionName, FixAllScope? fixAllScope, bool blockUntilComplete, CancellationToken cancellationToken)
        {
            var lightBulbAction = GetLightBulbApplicationAction(actionName, fixAllScope, blockUntilComplete);

            var listenerProvider = await GetComponentModelServiceAsync<IAsynchronousOperationListenerProvider>(cancellationToken);
            var listener = listenerProvider.GetListener(FeatureAttribute.LightBulb);

            var task = JoinableTaskFactory.RunAsync(async () =>
            {
                using var _ = listener.BeginAsyncOperation(nameof(ApplyLightBulbActionAsync));

                await JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);

                var activeTextView = await GetActiveTextViewAsync(cancellationToken);
                return await lightBulbAction(activeTextView, cancellationToken);
            });

            if (blockUntilComplete)
            {
                var result = await task.JoinAsync(cancellationToken);
                await DismissLightBulbSessionAsync(cancellationToken);
                return result;
            }

            return true;
        }

        private Func<IWpfTextView, CancellationToken, Task<bool>> GetLightBulbApplicationAction(string actionName, FixAllScope? fixAllScope, bool willBlockUntilComplete)
        {
            return async (view, cancellationToken) =>
            {
                await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

                var broker = await GetComponentModelServiceAsync<ILightBulbBroker>(cancellationToken);

                var actions = (await GetLightBulbActionsAsync(broker, view, cancellationToken)).ToArray();
                var action = actions.FirstOrDefault(a => a.DisplayText == actionName);

                if (action == null)
                {
                    var sb = new StringBuilder();
                    foreach (var item in actions)
                    {
                        sb.AppendLine("Actual ISuggestedAction: " + item.DisplayText);
                    }

                    var bufferType = view.TextBuffer.ContentType.DisplayName;
                    throw new InvalidOperationException(
                        $"ISuggestedAction {actionName} not found.  Buffer content type={bufferType}\r\nActions: {sb}");
                }

                if (fixAllScope != null)
                {
                    if (!action.HasActionSets)
                    {
                        throw new InvalidOperationException($"Suggested action '{action.DisplayText}' does not support FixAllOccurrences.");
                    }

                    var actionSetsForAction = await action.GetActionSetsAsync(cancellationToken);
                    var fixAllAction = await GetFixAllSuggestedActionAsync(actionSetsForAction!, fixAllScope.Value, cancellationToken);
                    if (fixAllAction == null)
                    {
                        throw new InvalidOperationException($"Unable to find FixAll in {fixAllScope} code fix for suggested action '{action.DisplayText}'.");
                    }

                    action = fixAllAction;

                    if (willBlockUntilComplete
                        && action is AbstractFixAllSuggestedAction fixAllSuggestedAction
                        && fixAllSuggestedAction.CodeAction is AbstractFixAllCodeAction fixAllCodeAction)
                    {
                        // Ensure the preview changes dialog will not be shown. Since the operation 'willBlockUntilComplete',
                        // the caller would not be able to interact with the preview changes dialog, and the tests would
                        // either timeout or deadlock.
                        fixAllCodeAction.GetTestAccessor().ShowPreviewChangesDialog = false;
                    }

                    if (string.IsNullOrEmpty(actionName))
                    {
                        return false;
                    }

                    // Dismiss the lightbulb session as we not invoking the original code fix.
                    broker.DismissSession(view);
                }

                if (action is not SuggestedAction suggestedAction)
                    return true;

                broker.DismissSession(view);
                var threadOperationExecutor = await GetComponentModelServiceAsync<IUIThreadOperationExecutor>(cancellationToken);
                var guardedOperations = await GetComponentModelServiceAsync<IGuardedOperations2>(cancellationToken);
                threadOperationExecutor.Execute(
                    title: "Execute Suggested Action",
                    defaultDescription: Accelerator.StripAccelerators(action.DisplayText, '_'),
                    allowCancellation: true,
                    showProgress: true,
                    action: context =>
                    {
                        guardedOperations.CallExtensionPoint(
                            errorSource: suggestedAction,
                            call: () => suggestedAction.Invoke(context),
                            exceptionGuardFilter: e => e is not OperationCanceledException);
                    });

                return true;
            };
        }

        private async Task<IEnumerable<ISuggestedAction>> GetLightBulbActionsAsync(ILightBulbBroker broker, IWpfTextView view, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            if (!broker.IsLightBulbSessionActive(view))
            {
                var bufferType = view.TextBuffer.ContentType.DisplayName;
                throw new Exception($"No light bulb session in View!  Buffer content type={bufferType}");
            }

            var activeSession = broker.GetSession(view);
            if (activeSession == null)
            {
                var bufferType = view.TextBuffer.ContentType.DisplayName;
                throw new InvalidOperationException($"No expanded light bulb session found after View.ShowSmartTag.  Buffer content type={bufferType}");
            }

            var actionSets = await LightBulbHelper.WaitForItemsAsync(TestServices, broker, view, cancellationToken);
            return await SelectActionsAsync(actionSets, cancellationToken);
        }

        private async Task<IEnumerable<ISuggestedAction>> SelectActionsAsync(IEnumerable<SuggestedActionSet> actionSets, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var actions = new List<ISuggestedAction>();

            if (actionSets != null)
            {
                foreach (var actionSet in actionSets)
                {
                    if (actionSet.Actions != null)
                    {
                        foreach (var action in actionSet.Actions)
                        {
                            actions.Add(action);
                            if (action.HasActionSets)
                            {
                                var nestedActionSets = await action.GetActionSetsAsync(cancellationToken);
                                var nestedActions = await SelectActionsAsync(nestedActionSets!, cancellationToken);
                                actions.AddRange(nestedActions);
                            }
                        }
                    }
                }
            }

            return actions;
        }

        private async Task<AbstractFixAllSuggestedAction?> GetFixAllSuggestedActionAsync(IEnumerable<SuggestedActionSet> actionSets, FixAllScope fixAllScope, CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            foreach (var actionSet in actionSets)
            {
                foreach (var action in actionSet.Actions)
                {
                    if (action is AbstractFixAllSuggestedAction fixAllSuggestedAction)
                    {
                        var fixAllCodeAction = fixAllSuggestedAction.CodeAction as AbstractFixAllCodeAction;
                        if (fixAllCodeAction?.FixAllState?.Scope == fixAllScope)
                        {
                            return fixAllSuggestedAction;
                        }
                    }

                    if (action.HasActionSets)
                    {
                        var nestedActionSets = await action.GetActionSetsAsync(cancellationToken);
                        var fixAllCodeAction = await GetFixAllSuggestedActionAsync(nestedActionSets!, fixAllScope, cancellationToken);
                        if (fixAllCodeAction != null)
                        {
                            return fixAllCodeAction;
                        }
                    }
                }
            }

            return null;
        }

        public Task PlaceCaretAsync(string marker, int charsOffset, CancellationToken cancellationToken)
            => PlaceCaretAsync(marker, charsOffset, occurrence: 0, extendSelection: false, selectBlock: false, cancellationToken);

        public async Task PlaceCaretAsync(
            string marker,
            int charsOffset,
            int occurrence,
            bool extendSelection,
            bool selectBlock,
            CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var dte = await GetRequiredGlobalServiceAsync<SDTE, EnvDTE.DTE>(cancellationToken);
            dte.Find.FindWhat = marker;
            dte.Find.MatchCase = true;
            dte.Find.MatchInHiddenText = true;
            dte.Find.Target = EnvDTE.vsFindTarget.vsFindTargetCurrentDocument;
            dte.Find.Action = EnvDTE.vsFindAction.vsFindActionFind;

            var originalPosition = await GetCaretPositionAsync(cancellationToken);
            view.Caret.MoveTo(new SnapshotPoint(view.GetBufferContainingCaret()!.CurrentSnapshot, 0));

            if (occurrence > 0)
            {
                var result = EnvDTE.vsFindResult.vsFindResultNotFound;
                for (var i = 0; i < occurrence; i++)
                {
                    result = dte.Find.Execute();
                }

                if (result != EnvDTE.vsFindResult.vsFindResultFound)
                {
                    throw new Exception("Occurrence " + occurrence + " of marker '" + marker + "' not found in text: " + view.TextSnapshot.GetText());
                }
            }
            else
            {
                var result = dte.Find.Execute();
                if (result != EnvDTE.vsFindResult.vsFindResultFound)
                {
                    throw new Exception("Marker '" + marker + "' not found in text: " + view.TextSnapshot.GetText());
                }
            }

            if (charsOffset > 0)
            {
                for (var i = 0; i < charsOffset - 1; i++)
                {
                    view.Caret.MoveToNextCaretPosition();
                }

                view.Selection.Clear();
            }

            if (charsOffset < 0)
            {
                // On the first negative charsOffset, move to anchor-point position, as if the user hit the LEFT key
                view.Caret.MoveTo(new SnapshotPoint(view.TextSnapshot, view.Selection.AnchorPoint.Position.Position));

                for (var i = 0; i < -charsOffset - 1; i++)
                {
                    view.Caret.MoveToPreviousCaretPosition();
                }

                view.Selection.Clear();
            }

            if (extendSelection)
            {
                var newPosition = view.Selection.ActivePoint.Position.Position;
                view.Selection.Select(new VirtualSnapshotPoint(view.TextSnapshot, originalPosition), new VirtualSnapshotPoint(view.TextSnapshot, newPosition));
                view.Selection.Mode = selectBlock ? TextSelectionMode.Box : TextSelectionMode.Stream;
            }
        }

        public async Task<int> GetCaretPositionAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var view = await GetActiveTextViewAsync(cancellationToken);

            var subjectBuffer = view.GetBufferContainingCaret();
            Assumes.Present(subjectBuffer);

            var bufferPosition = view.Caret.Position.BufferPosition;
            return bufferPosition.Position;
        }

        public async Task GoToDefinitionAsync(CancellationToken cancellationToken)
        {
            await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd97CmdID.GotoDefn, cancellationToken);
            await TestServices.Workspace.WaitForAllAsyncOperationsAsync(
                new[] { FeatureAttribute.Workspace, FeatureAttribute.NavigateTo, FeatureAttribute.GoToDefinition },
                cancellationToken);
        }

        public async Task GoToBaseAsync(CancellationToken cancellationToken)
        {
            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            await TestServices.Shell.ExecuteCommandAsync(EditorConstants.EditorCommandID.GoToBase, cancellationToken);

            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.Workspace, cancellationToken);

            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.GoToBase, cancellationToken);
            await TestServices.Editor.WaitForEditorOperationsAsync(cancellationToken);
        }

        public async Task ConfigureAsyncNavigation(AsyncNavigationKind kind, CancellationToken cancellationToken)
        {
            Func<CancellationToken, Task>? delayHook = kind switch
            {
                AsyncNavigationKind.Default => null,
                AsyncNavigationKind.Synchronous => static cancellationToken => Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken),
                AsyncNavigationKind.Asynchronous => static _ => Task.CompletedTask,
                _ => throw ExceptionUtilities.UnexpectedValue(kind),
            };

            var componentModelService = await GetRequiredGlobalServiceAsync<SComponentModel, IComponentModel>(cancellationToken);
            var commandHandlers = componentModelService.DefaultExportProvider.GetExports<ICommandHandler, NameMetadata>();
            var goToImplementation = (GoToImplementationCommandHandler)commandHandlers.Single(handler => handler.Metadata.Name == PredefinedCommandHandlerNames.GoToImplementation).Value;
            goToImplementation.GetTestAccessor().DelayHook = delayHook;

            var goToBase = (GoToBaseCommandHandler)commandHandlers.Single(handler => handler.Metadata.Name == PredefinedCommandHandlerNames.GoToBase).Value;
            goToBase.GetTestAccessor().DelayHook = delayHook;
        }

        public async Task GoToImplementationAsync(CancellationToken cancellationToken)
        {
            await TestServices.Shell.ExecuteCommandAsync(WellKnownCommands.Edit.GoToImplementation, cancellationToken);
            await TestServices.Workspace.WaitForAllAsyncOperationsAsync(
                new[] { FeatureAttribute.Workspace, FeatureAttribute.GoToImplementation },
                cancellationToken);
        }

        public async Task<ImmutableArray<(bool Collapsed, TextSpan Span)>> GetOutliningSpansAsync(CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAllAsyncOperationsAsync(
                new[] { FeatureAttribute.Workspace, FeatureAttribute.Outlining },
                cancellationToken);

            await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            var componentModelService = await GetRequiredGlobalServiceAsync<SComponentModel, IComponentModel>(cancellationToken);
            var view = await GetActiveTextViewAsync(cancellationToken);
            var manager = componentModelService.GetService<IOutliningManagerService>().GetOutliningManager(view);
            var span = new SnapshotSpan(view.TextSnapshot, 0, view.TextSnapshot.Length);
            var regions = manager.GetAllRegions(span);
            return regions
                    .OrderBy(s => s.Extent.GetStartPoint(view.TextSnapshot))
                    .SelectAsArray(r =>
                    {
                        var span = r.Extent.GetSpan(view.TextSnapshot);
                        return (r.IsCollapsed, TextSpan.FromBounds(span.Start.Position, span.End.Position));
                    });
        }

        public Task FormatDocumentAsync(CancellationToken cancellationToken)
        {
            return TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd2KCmdID.FORMATDOCUMENT, cancellationToken);
        }

        public Task FormatSelectionAsync(CancellationToken cancellationToken)
        {
            return TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd2KCmdID.FORMATSELECTION, cancellationToken);
        }

        private async Task WaitForCompletionSetAsync(CancellationToken cancellationToken)
        {
            await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.CompletionSet, cancellationToken);
        }
    }
}
