// 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.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion;

[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.Completion)]
public sealed class CompletionServiceTests
{
    [Fact]
    public void AcquireCompletionService()
    {
        var workspace = new AdhocWorkspace();

        var document = workspace
            .AddProject("TestProject", LanguageNames.CSharp)
            .AddDocument("TestDocument.cs", "");

        var service = CompletionService.GetService(document);
        Assert.NotNull(service);
    }

    [Fact]
    public void FindCompletionProvider()
    {
        using var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features.AddParts(typeof(ThirdPartyCompletionProvider)));
        var text = SourceText.From("class C { }");

        var document = workspace.CurrentSolution
            .AddProject("TestProject", "Assembly", LanguageNames.CSharp)
            .AddDocument("TestDocument.cs", text);

        var service = CompletionService.GetService(document);

        // Create an item with ProviderName set to ThirdPartyCompletionProvider
        // We should be able to find the provider object without calling into CompletionService for other operations.
        var item = CompletionItem.Create("ThirdPartyCompletionProviderItem");
        item.ProviderName = typeof(ThirdPartyCompletionProvider).FullName;

        var provider = service.GetProvider(item, document.Project);
        Assert.True(provider is ThirdPartyCompletionProvider);
    }

    [ExportCompletionProvider(nameof(ThirdPartyCompletionProvider), LanguageNames.CSharp)]
    [ExtensionOrder(After = nameof(KeywordCompletionProvider))]
    [Shared]
    private sealed class ThirdPartyCompletionProvider : CompletionProvider
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public ThirdPartyCompletionProvider()
        {
        }

        public override Task ProvideCompletionsAsync(CompletionContext context)
            => Task.CompletedTask;

        public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
        {
            Assert.Equal(1, options.GetOption(new OptionKey(ThirdPartyOption.Instance, LanguageNames.CSharp)));
            return true;
        }
    }

    private sealed class ThirdPartyOption : IOption
    {
        public static ThirdPartyOption Instance = new();

        public string Feature => "TestOptions";
        public string Name => "Option";
        public Type Type => typeof(int);
        public object DefaultValue => 0;
        public bool IsPerLanguage => true;
        public ImmutableArray<OptionStorageLocation> StorageLocations => [];
    }

    /// <summary>
    /// Ensure that 3rd party can set options on solution and access them from within a custom completion provider.
    /// </summary>
    [Fact]
    public async Task PassThroughOptions1()
    {
        using var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features.AddParts(typeof(ThirdPartyCompletionProvider)));

        var text = SourceText.From("class C { }");

        var document = workspace.CurrentSolution
            .AddProject("TestProject", "Assembly", LanguageNames.CSharp)
            .AddDocument("TestDocument.cs", text);

        var service = CompletionService.GetService(document);
        var options = new TestOptionSet(ImmutableDictionary<OptionKey, object>.Empty.Add(new OptionKey(ThirdPartyOption.Instance, LanguageNames.CSharp), 1));
        service.ShouldTriggerCompletion(text, 1, CompletionTrigger.Invoke, options: options);

#pragma warning disable RS0030 // Do not used banned APIs
        await service.GetCompletionsAsync(document, 1, CompletionTrigger.Invoke, options: options);
#pragma warning restore
    }

    /// <summary>
    /// Ensure that 3rd party can set options on solution and access them from within a custom completion provider.
    /// </summary>
    [Fact]
    public async Task PassThroughOptions2()
    {
        using var workspace = new EditorTestWorkspace(composition: EditorTestCompositions.EditorFeatures.AddParts(typeof(ThirdPartyCompletionProvider)));

        var testDocument = new EditorTestHostDocument("class C {}");
        var project = new EditorTestHostProject(workspace, testDocument, name: "project1");
        workspace.AddTestProject(project);
        workspace.OpenDocument(testDocument.Id);

        Assert.True(workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(
            workspace.CurrentSolution.Options.WithChangedOption(new OptionKey(ThirdPartyOption.Instance, LanguageNames.CSharp), 1))));

        var document = workspace.CurrentSolution.GetDocument(testDocument.Id);
        var text = await document.GetTextAsync();

        var service = CompletionService.GetService(document);
        service.ShouldTriggerCompletion(text, 1, CompletionTrigger.Invoke, options: null);

#pragma warning disable RS0030 // Do not used banned APIs
        await service.GetCompletionsAsync(document, 1, CompletionTrigger.Invoke, options: null);
#pragma warning restore
    }

    [Theory, CombinatorialData]
    public async Task GettingCompletionListPerformSort(bool performSort)
    {
        var sourceMarkup = """
            using System;

            namespace N
            {
                public class C
                {
                    void M()
                    {
                        $$
                    }
                }
            }
            """;
        MarkupTestFile.GetPosition(sourceMarkup.NormalizeLineEndings(), out var source, out int? position);

        var workspace = new AdhocWorkspace();

        var document = workspace
            .AddProject("TestProject", LanguageNames.CSharp)
            .AddDocument("TestDocument.cs", source);

        var completionService = document.GetLanguageService<CompletionService>();

        var options = CompletionOptions.Default with { PerformSort = performSort };
        var completionList = await completionService.GetCompletionsAsync(document, position.Value, options, OptionSet.Empty);

        var completionListManuallySorted = completionList.ItemsList.ToList();
        completionListManuallySorted.Sort();

        Assert.True(performSort == completionList.ItemsList.SequenceEqual(completionListManuallySorted));
    }

    [Theory, CombinatorialData]
    public async Task GettingCompletionListShouldNotRunSourceGenerator(bool forkBeforeFreeze)
    {
        var sourceMarkup = """
            using System;

            namespace N
            {
                public class C1
                {
                    $$
                }
            }
            """;
        MarkupTestFile.GetPosition(sourceMarkup.NormalizeLineEndings(), out var source, out int? position);

        var generatorRanCount = 0;
        var generator = new CallbackGenerator(onInit: _ => { }, onExecute: _ => Interlocked.Increment(ref generatorRanCount));

        using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
        var analyzerReference = new TestGeneratorReference(generator);
        var project = SolutionUtilities.AddEmptyProject(workspace.CurrentSolution)
            .AddAnalyzerReference(analyzerReference)
            .AddDocument("Document1.cs", sourceMarkup, filePath: "Document1.cs").Project;

        Assert.True(workspace.SetCurrentSolution(_ => project.Solution, WorkspaceChangeKind.SolutionChanged));

        var document = workspace.CurrentSolution.Projects.Single().Documents.Single();
        var completionService = document.GetLanguageService<CompletionService>();

        Assert.Equal(0, generatorRanCount);

        if (forkBeforeFreeze)
        {
            // Forking before freezing means we'll have to do extra work to produce the final compilation,
            // but we should still not be running generators. 
            document = document.WithText(SourceText.From(sourceMarkup.Replace("C1", "C2")));
        }

        // We want to make sure import completion providers are also participating.
        var options = CompletionOptions.Default with { ShowItemsFromUnimportedNamespaces = true };
        var completionList = await completionService.GetCompletionsAsync(document, position.Value, options, OptionSet.Empty);

        // We expect completion to run on frozen partial semantic, which won't run source generator.
        Assert.Equal(0, generatorRanCount);

        var expectedItem = forkBeforeFreeze ? "C2" : "C1";
        Assert.True(completionList.ItemsList.Select(item => item.DisplayText).Contains(expectedItem));
    }
}
