﻿// 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.Collections.Immutable;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste;
using Microsoft.CodeAnalysis.Editor.StringCopyPaste;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.StringCopyPaste
{
    [UseExportProvider]
    public abstract class StringCopyPasteCommandHandlerTests
    {
        internal sealed class StringCopyPasteTestState : AbstractCommandHandlerTestState
        {
            private static readonly TestComposition s_composition =
                EditorTestCompositions.EditorFeaturesWpf
                    .AddParts(typeof(StringCopyPasteCommandHandler));

            private static readonly TestComposition s_compositionWithMockCopyPasteService =
                EditorTestCompositions.EditorFeaturesWpf
                    .RemoveExcludedPartTypes(typeof(WpfStringCopyPasteService))
                    .AddParts(typeof(TestStringCopyPasteService))
                    .AddParts(typeof(StringCopyPasteCommandHandler));

            public readonly StringCopyPasteCommandHandler CommandHandler;

            public StringCopyPasteTestState(XElement workspaceElement, bool mockCopyPasteService)
                : base(workspaceElement, mockCopyPasteService
                      ? s_compositionWithMockCopyPasteService
                      : s_composition)
            {
                CommandHandler = (StringCopyPasteCommandHandler)GetExportedValues<ICommandHandler>().
                    Single(c => c is StringCopyPasteCommandHandler);
            }

            public static StringCopyPasteTestState CreateTestState(string? copyFileMarkup, string pasteFileMarkup, bool mockCopyPasteService)
                => new(GetWorkspaceXml(copyFileMarkup, pasteFileMarkup), mockCopyPasteService);

            public static XElement GetWorkspaceXml(string? copyFileMarkup, string pasteFileMarkup)
                => XElement.Parse(($@"
<Workspace>
    <Project Language=""C#"" CommonReferences=""true"">
        <Document Markup=""SpansOnly"">{pasteFileMarkup}</Document>
    </Project>
    {(copyFileMarkup == null ? "" : $@"
    <Project Language=""C#"" CommonReferences=""true"">
        <Document Markup=""SpansOnly"">{copyFileMarkup}</Document>
    </Project>")}
</Workspace>"));

            internal void AssertCodeIs(string expectedCode)
            {
                TestFileMarkupParser.GetPositionAndSpans(
                    expectedCode, out var massaged, out int? caretPosition, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spans);
                Assert.Equal(massaged, TextView.TextSnapshot.GetText());
                Assert.Equal(caretPosition, TextView.Caret.Position.BufferPosition.Position);

                var virtualSpaces = spans.SingleOrDefault(kvp => kvp.Key.StartsWith("VirtualSpaces#"));
                if (virtualSpaces.Key != null)
                {
                    var virtualOffset = int.Parse(virtualSpaces.Key["VirtualSpaces-".Length..]);
                    Assert.True(TextView.Caret.InVirtualSpace);
                    Assert.Equal(virtualOffset, TextView.Caret.Position.VirtualBufferPosition.VirtualSpaces);
                }
            }

            public void TestCopyPaste(string expectedMarkup, string? pasteText, bool pasteTextIsKnown, string afterUndoMarkup)
            {
                var workspace = this.Workspace;

                // Ensure we clear out the clipboard so that a prior copy/paste doesn't corrupt the test.
                var service = workspace.Services.GetRequiredService<IStringCopyPasteService>() as TestStringCopyPasteService;
                service?.TrySetClipboardData(StringCopyPasteCommandHandler.KeyAndVersion, "");

                var copyDocument = this.Workspace.Documents.FirstOrDefault(d => d.AnnotatedSpans.ContainsKey("Copy"));
                if (copyDocument != null)
                {
                    Assert.Null(pasteText);
                    var copySpans = copyDocument.AnnotatedSpans["Copy"];

                    SetSelection(copyDocument, copySpans, out var copyTextView, out var copyTextBuffer);

                    CommandHandler.ExecuteCommand(
                        new CopyCommandArgs(copyTextView, copyTextBuffer), () =>
                        {
                            var copyEditorOperations = GetService<IEditorOperationsFactoryService>().GetEditorOperations(copyTextView);
                            Assert.True(copyEditorOperations.CopySelection());
                        }, TestCommandExecutionContext.Create());
                }
                else
                {
                    // If we don't have a file to copy text from, then the paste text must be explicitly provided.");
                    Assert.NotNull(pasteText);
                }

                if (pasteText == null)
                {
                    // if the user didn't supply text to paste, then just paste in what we put in the clipboard above.
                    CommandHandler.ExecuteCommand(
                        new PasteCommandArgs(this.TextView, this.SubjectBuffer), () => EditorOperations.Paste(), TestCommandExecutionContext.Create());
                }
                else
                {
                    // otherwise, this is a test of text coming in from another source.  Do the edit manually.

                    if (pasteTextIsKnown)
                    {
                        // we were given text to directly place on the clipboard without needing to do a copy.
                        Contract.ThrowIfNull(pasteText);
                        var json = new StringCopyPasteData(ImmutableArray.Create(StringCopyPasteContent.ForText(pasteText))).ToJson();
                        Contract.ThrowIfNull(json);
                        service!.TrySetClipboardData(StringCopyPasteCommandHandler.KeyAndVersion, json);
                    }

                    CommandHandler.ExecuteCommand(
                        new PasteCommandArgs(this.TextView, this.SubjectBuffer), () =>
                        {
                            EditorOperations.ReplaceSelection(pasteText);
                        }, TestCommandExecutionContext.Create());
                }

                ValidateBefore(expectedMarkup);

                this.SendUndo();

                ValidateAfter(afterUndoMarkup);
            }

            private void ValidateBefore(string expectedMarkup)
            {
                GetCodeAndCaretPosition(expectedMarkup, out var expected, out var caretPosition);
                var finalText = this.SubjectBuffer.CurrentSnapshot.GetText();

                Assert.Equal(expected, finalText);
                Assert.Equal(caretPosition, this.TextView.Caret.Position.BufferPosition.Position);
            }

            private static void GetCodeAndCaretPosition(string expectedMarkup, out string expected, out int caretPosition)
            {
                // Used so test can contain `$$` (for raw interpolations) without us thinking that it is an actual caret
                // position
                const string NON_TEST_CHARACTER = "\uD7FF";

                expectedMarkup = expectedMarkup.Replace("$", NON_TEST_CHARACTER);

                TestFileMarkupParser.GetSpan(expectedMarkup, out expected, out var caretSpan);

                expected = expected.Replace(NON_TEST_CHARACTER, "$");

                caretPosition = caretSpan.Start;
            }

            private void ValidateAfter(string afterUndoMarkup)
            {
                GetCodeAndCaretPosition(afterUndoMarkup, out var expected, out var caretPosition);
                var finalText = this.SubjectBuffer.CurrentSnapshot.GetText();

                Assert.Equal(expected, finalText);
                Assert.Equal(caretPosition, this.TextView.Caret.Position.BufferPosition.Position);
            }

            private static void SetSelection(
                TestHostDocument document,
                ImmutableArray<TextSpan> copySpans, out IWpfTextView textView, out ITextBuffer2 textBuffer2)
            {
                textView = document.GetTextView();
                var textBuffer = document.GetTextBuffer();
                textBuffer2 = textBuffer;
                var broker = textView.GetMultiSelectionBroker();

                var selections = copySpans.Select(s => new Selection(s.ToSnapshotSpan(textBuffer.CurrentSnapshot))).ToArray();
                broker.SetSelectionRange(selections, selections.First());
            }
        }
    }
}
