// 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.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders
{
    [Trait(Traits.Feature, Traits.Features.Completion)]
    public class SnippetCompletionProviderTests : AbstractCSharpCompletionProviderTests
    {
        public SnippetCompletionProviderTests()
        {
            ShowNewSnippetExperience = false;
        }

        internal override Type GetCompletionProviderType()
            => typeof(SnippetCompletionProvider);

        protected override TestComposition GetComposition()
            => base.GetComposition().AddParts(typeof(MockSnippetInfoService));

        [Fact]
        public async Task SnippetsInEmptyFile()
            => await VerifyItemExistsAsync(@"$$", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetDescriptions()
            => await VerifyItemExistsAsync(@"$$", MockSnippetInfoService.SnippetShortcut, MockSnippetInfoService.SnippetTitle + Environment.NewLine + MockSnippetInfoService.SnippetDescription + Environment.NewLine + string.Format(FeaturesResources.Note_colon_Tab_twice_to_insert_the_0_snippet, MockSnippetInfoService.SnippetShortcut), SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsInNamespace()
            => await VerifyItemExistsAsync(@"namespace NS { $$ }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsInClass()
            => await VerifyItemExistsAsync(@"namespace NS { class C { $$ } }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsInMethod()
            => await VerifyItemExistsAsync(@"namespace NS { class C { void M() { $$ } } }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsNotInLocalDeclarationIdentifier()
            => await VerifyItemIsAbsentAsync(@"namespace NS { class C { void M() { int $$ } } }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsNotInEnum()
            => await VerifyItemIsAbsentAsync(@"namespace NS { enum E { $$ } }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsInExpression()
            => await VerifyItemExistsAsync(@"namespace NS { class C { void M() { bool b = true && $$ } } }", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/608860")]
        public async Task SnippetsInPreProcessorContextWhenShortcutBeginsWithHash()
            => await VerifyItemExistsAsync(@"#$$", MockSnippetInfoService.PreProcessorSnippetShortcut[1..], sourceCodeKind: SourceCodeKind.Regular);

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/608860")]
        public async Task SnippetsNotInPreProcessorContextWhenShortcutDoesNotBeginWithHash()
            => await VerifyItemIsAbsentAsync(@"#$$", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/770156")]
        public async Task SnippetsNotInPreProcessorContextDirectiveNameAlreadyTyped()
            => await VerifyItemIsAbsentAsync(@"#region $$", MockSnippetInfoService.PreProcessorSnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/839555")]
        public async Task ShowRegionSnippetWithHashRTyped()
            => await VerifyItemExistsAsync(@"#r$$", MockSnippetInfoService.PreProcessorSnippetShortcut[1..], sourceCodeKind: SourceCodeKind.Regular);

        [Fact]
        public async Task SnippetsInLineSpanDirective()
            => await VerifyItemIsAbsentAsync(@"#line (1, 2) - (3, 4) $$", MockSnippetInfoService.PreProcessorSnippetShortcut, sourceCodeKind: SourceCodeKind.Regular);

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/968256")]
        public async Task ShowSnippetsFromOtherContext()
        {
            var markup = """
                <Workspace>
                    <Project Language="C#" CommonReferences="true" AssemblyName="Proj1">
                        <Document FilePath="CurrentDocument.cs"><![CDATA[
                class C
                {
                #if GOO
                    $$
                #endif
                }
                ]]>
                        </Document>
                    </Project>
                    <Project Language="C#" CommonReferences="true" AssemblyName="Proj2"  PreprocessorSymbols="GOO">
                        <Document IsLinkFile="true" LinkAssemblyName="Proj1" LinkFilePath="CurrentDocument.cs"/>
                    </Project>
                </Workspace>
                """;
            await VerifyItemInLinkedFilesAsync(markup, MockSnippetInfoService.SnippetShortcut, null);
        }

        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1140893")]
        public async Task CommitWithEnterObeysOption()
        {
            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcu", sendThroughEnterOption: EnterKeyRule.Always, expected: true);
            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcut", sendThroughEnterOption: EnterKeyRule.Always, expected: true);

            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcu", sendThroughEnterOption: EnterKeyRule.AfterFullyTypedWord, expected: false);
            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcut", sendThroughEnterOption: EnterKeyRule.AfterFullyTypedWord, expected: true);

            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcu", sendThroughEnterOption: EnterKeyRule.Never, expected: false);
            await VerifySendEnterThroughToEnterAsync("$$", "SnippetShortcut", sendThroughEnterOption: EnterKeyRule.Never, expected: false);
        }

        [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/6405")]
        public async Task SnippetsNotInPreProcessorContextForScriptDirectives()
        {
            await VerifyItemIsAbsentAsync(@"#r f$$", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Script);
            await VerifyItemIsAbsentAsync(@"#load f$$", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Script);
            await VerifyItemIsAbsentAsync(@"#!$$", MockSnippetInfoService.SnippetShortcut, sourceCodeKind: SourceCodeKind.Script);
        }

        [ExportLanguageService(typeof(ISnippetInfoService), LanguageNames.CSharp, ServiceLayer.Test), Shared, PartNotDiscoverable]
        private class MockSnippetInfoService : ISnippetInfoService
        {
            internal const string SnippetShortcut = nameof(SnippetShortcut);
            internal const string SnippetDescription = nameof(SnippetDescription);
            internal const string SnippetTitle = nameof(SnippetTitle);
            internal const string SnippetPath = nameof(SnippetPath);

            internal const string PreProcessorSnippetShortcut = "#PreProcessorSnippetShortcut";
            internal const string PreProcessorSnippetDescription = nameof(PreProcessorSnippetDescription);
            internal const string PreProcessorSnippetTitle = "#PreProcessorSnippetTitle";
            internal const string PreProcessorSnippetPath = nameof(PreProcessorSnippetPath);

            [ImportingConstructor]
            [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
            public MockSnippetInfoService()
            {
            }

            public IEnumerable<SnippetInfo> GetSnippetsIfAvailable()
                => new List<SnippetInfo>
                    {
                        new SnippetInfo(SnippetShortcut, SnippetTitle, SnippetDescription, SnippetPath),
                        new SnippetInfo(PreProcessorSnippetShortcut, PreProcessorSnippetTitle, PreProcessorSnippetDescription, PreProcessorSnippetPath)
                    };

            public bool SnippetShortcutExists_NonBlocking(string shortcut)
                => string.Equals(shortcut, SnippetShortcut, StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(shortcut, PreProcessorSnippetShortcut, StringComparison.OrdinalIgnoreCase);

            public bool ShouldFormatSnippet(SnippetInfo snippetInfo)
                => false;
        }
    }
}
