File: Completion\LanguageServerTagHelperCompletionServiceTest.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests\Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Test.Common;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.CodeAnalysis.Razor.Completion;
 
public class LanguageServerTagHelperCompletionServiceTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
    [Fact]
    [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1452432")]
    public void GetAttributeCompletions_OnlyIndexerNamePrefix()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("FormTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "form")
                .BoundAttributeDescriptor(attribute => attribute
                    .TypeName("System.Collections.Generic.IDictionary<System.String, System.String>")
                    .PropertyName("RouteValues")
                    .AsDictionary("asp-route-", typeof(string).FullName))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["asp-route-..."] = [tagHelpers[0].BoundAttributes[^1]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: [],
            attributes: [],
            currentTagName: "form");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_BoundDictionaryAttribute_ReturnsPrefixIndexerAndFullSetter()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("FormTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "form")
                .BoundAttributeDescriptor(attribute => attribute
                    .Name("asp-all-route-data")
                    .TypeName("System.Collections.Generic.IDictionary<System.String, System.String>")
                    .PropertyName("RouteValues")
                    .AsDictionary("asp-route-", typeof(string).FullName))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["asp-all-route-data"] = [tagHelpers[0].BoundAttributes[^1]],
            ["asp-route-..."] = [tagHelpers[0].BoundAttributes[^1]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: [],
            attributes: [],
            currentTagName: "form");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_RequiredBoundDictionaryAttribute_ReturnsPrefixIndexerAndFullSetter()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("FormTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "form", static b => b
                    .RequiredAttribute(name: "asp-route-", nameComparison: RequiredAttributeNameComparison.PrefixMatch))
                .TagMatchingRule(tagName: "form", static b => b
                    .RequiredAttribute(name: "asp-all-route-data"))
                .BoundAttributeDescriptor(attribute => attribute
                    .Name("asp-all-route-data")
                    .TypeName("System.Collections.Generic.IDictionary<System.String, System.String>")
                    .PropertyName("RouteValues")
                    .AsDictionary("asp-route-", typeof(string).FullName))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["asp-all-route-data"] = [tagHelpers[0].BoundAttributes[^1]],
            ["asp-route-..."] = [tagHelpers[0].BoundAttributes[^1]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: [],
            attributes: [],
            currentTagName: "form");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_DoesNotReturnCompletionsForAlreadySuppliedAttributes()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .BoundAttribute<string>(name: "class", propertyName: "Class")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["onclick"] = [],
            ["visible"] = [tagHelpers[0].BoundAttributes[^1]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["onclick"],
            attributes: [
                KeyValuePair.Create("class", "something"),
                KeyValuePair.Create("repeat", "4")],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_ReturnsCompletionForAlreadySuppliedAttribute_IfCurrentAttributeMatches()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .BoundAttribute<string>(name: "class", propertyName: "Class")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["onclick"] = [],
            ["visible"] = [tagHelpers[0].BoundAttributes[^1]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["onclick"],
            attributes: [
                KeyValuePair.Create("class", "something"),
                KeyValuePair.Create("repeat", "4"),
                KeyValuePair.Create("visible", "false")],
            currentTagName: "div",
            currentAttributeName: "visible");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_DoesNotReturnAlreadySuppliedAttribute_IfCurrentAttributeDoesNotMatch()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .BoundAttribute<string>(name: "class", propertyName: "Class")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["onclick"] = []
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["onclick"],
            attributes: [
                KeyValuePair.Create("class", "something"),
                KeyValuePair.Create("repeat", "4"),
                KeyValuePair.Create("visible", "false")],
            currentTagName: "div",
            currentAttributeName: "repeat");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_PossibleDescriptorsReturnUnboundRequiredAttributesWithExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*", static b => b
                    .RequiredAttribute(name: "class"))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = [],
            ["onclick"] = [],
            ["repeat"] = []
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["onclick", "class"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_PossibleDescriptorsReturnBoundRequiredAttributesWithExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .BoundAttribute<bool>(name: "repeat", propertyName: "Repeat")
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*", static b => b
                    .RequiredAttribute(name: "class"))
                .BoundAttribute<string>(name: "class", propertyName: "Class")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = [.. tagHelpers[1].BoundAttributes],
            ["onclick"] = [],
            ["repeat"] = [tagHelpers[0].BoundAttributes[0]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["onclick"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_AppliedDescriptorsReturnAllBoundAttributesWithExistingCompletionsForSchemaTags()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute(name: "repeat"))
                .BoundAttribute<bool>(name: "repeat", propertyName: "Repeat")
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*", static b => b
                    .RequiredAttribute(name: "class"))
                .BoundAttribute<string>(name: "class", propertyName: "Class")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("StyleTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .BoundAttribute<bool>(name: "visible", propertyName: "Visible")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["onclick"] = [],
            ["class"] = [.. tagHelpers[1].BoundAttributes],
            ["repeat"] = [tagHelpers[0].BoundAttributes[0]],
            ["visible"] = [tagHelpers[0].BoundAttributes[^1], tagHelpers[2].BoundAttributes[0]]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class", "onclick"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_AppliedTagOutputHintDescriptorsReturnBoundAttributesWithExistingCompletionsForNonSchemaTags()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("CustomTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "custom")
                .BoundAttribute<bool>(name: "repeat", propertyName: "Repeat")
                .TagOutputHint("div")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = [],
            ["repeat"] = [.. tagHelpers[0].BoundAttributes]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class"],
            currentTagName: "custom");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesCompletionsForNonSchemaTags()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("CustomTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "custom")
                .BoundAttribute<bool>(name: "repeat", propertyName: "Repeat")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["repeat"] = [.. tagHelpers[0].BoundAttributes]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class"],
            currentTagName: "custom");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesWithExistingCompletionsForSchemaTags()
    {
        // Arrange
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .BoundAttribute<bool>(name: "repeat", propertyName: "Repeat")
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = [],
            ["repeat"] = [.. tagHelpers[0].BoundAttributes]
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_NoDescriptorsReturnsExistingCompletions()
    {
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = []
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers: [],
            existingCompletions: ["class"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_NoDescriptorsForUnprefixedTagReturnsExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div", static b => b
                    .RequiredAttribute("special"))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = []
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class"],
            currentTagName: "div",
            tagHelperPrefix: "th:");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetAttributeCompletions_NoDescriptorsForTagReturnsExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("MyTableTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "table", static b => b
                    .RequiredAttribute("special"))
                .Build(),
        ];
 
        var expectedCompletions = AttributeCompletionResult.Create(new()
        {
            ["class"] = []
        });
 
        var completionContext = BuildAttributeCompletionContext(
            tagHelpers,
            existingCompletions: ["class"],
            currentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetAttributeCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_IgnoresDirectiveAttributes()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BindAttribute", "TestAssembly")
                .TagMatchingRule(tagName: "input")
                .BoundAttributeDescriptor(builder =>
                {
                    builder.Name = "@bind";
                    builder.IsDirectiveAttribute = true;
                })
                .TagOutputHint("table")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create([]);
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["table"],
            containingTagName: "body",
            containingParentTagName: null);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_FiltersFullyQualifiedElementsIfShortNameExists()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "Test")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "TestAssembly.Test")
                .IsFullyQualifiedNameMatch(true)
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("Test2TagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "Test2Assembly.Test")
                .IsFullyQualifiedNameMatch(true)
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["Test"] = [tagHelpers[0]],
            ["Test2Assembly.Test"] = [tagHelpers[2]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "body",
            containingParentTagName: null);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_TagOutputHintDoesNotFallThroughToSchemaCheck()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("MyTableTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "my-table")
                .TagOutputHint("table")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("MyTrTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "my-tr")
                .TagOutputHint("tr")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["my-table"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["table", "div"],
            containingTagName: "body",
            containingParentTagName: null);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CatchAllsOnlyApplyToCompletionsStartingWithPrefix()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("CatchAllTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["th:li"] = [tagHelpers[1], tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul",
            tagHelperPrefix: "th:");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_TagHelperPrefixIsPrependedToTagHelperCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("SuperLiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "superli")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["th:superli"] = [tagHelpers[0]],
            ["th:li"] = [tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul",
            tagHelperPrefix: "th:");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_IsCaseSensitive()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("MyliTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "myli")
                .SetCaseSensitive()
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("MYLITagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "MYLI")
                .SetCaseSensitive()
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["myli"] = [tagHelpers[0]],
            ["MYLI"] = [tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_HTMLSchemaTagName_IsCaseSensitive()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("LITagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "LI")
                .SetCaseSensitive()
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .SetCaseSensitive()
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["LI"] = [tagHelpers[0]],
            ["li"] = [tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CatchAllsApplyToOnlyTagHelperCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("SuperLiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "superli")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("CatchAll", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .Build()
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["superli"] = [tagHelpers[0], tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CatchAllsApplyToNonTagHelperCompletionsIfStartsWithTagHelperPrefix()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("SuperLiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "superli")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("CatchAll", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["th:superli"] = [tagHelpers[0], tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["th:li"],
            containingTagName: "ul",
            tagHelperPrefix: "th:");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_AllowsMultiTargetingTagHelpers()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BoldTagHelper1", "TestAssembly")
                .TagMatchingRule(tagName: "strong")
                .TagMatchingRule(tagName: "b")
                .TagMatchingRule(tagName: "bold")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("BoldTagHelper2", "TestAssembly")
                .TagMatchingRule(tagName: "strong")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["strong"] = [tagHelpers[0], tagHelpers[1]],
            ["b"] = [tagHelpers[0]],
            ["bold"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["strong", "b", "bold"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CombinesDescriptorsOnExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper1", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper2", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["li"] = [tagHelpers[0], tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_NewCompletionsForSchemaTagsNotInExistingCompletionsAreIgnored()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("SuperLiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "superli")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .TagOutputHint("strong")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["li"] = [tagHelpers[1]],
            ["superli"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_OutputHintIsCrossReferencedWithExistingCompletions()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("DivTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .TagOutputHint("li")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .TagOutputHint("strong")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["div"] = [tagHelpers[0]],
            ["li"] = [tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_EnsuresDescriptorsHaveSatisfiedParent()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper1", "TestAssembly")
                .TagMatchingRule(tagName: "li")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("LiTagHelper2", "TestAssembly")
                .TagMatchingRule(tagName: "li", parentTagName: "ol")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["li"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["li"],
            containingTagName: "ul");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_NoContainingParentTag_DoesNotGetCompletionForRuleWithParentTag()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("Tag1", "TestAssembly")
                .TagMatchingRule(tagName: "outer-child-tag")
                .TagMatchingRule(tagName: "child-tag", parentTagName: "parent-tag")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("Tag2", "TestAssembly")
                .TagMatchingRule(tagName: "parent-tag")
                .AllowChildTag("child-tag")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["outer-child-tag"] = [tagHelpers[0]],
            ["parent-tag"] = [tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: null,
            containingParentTagName: null);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_WithContainingParentTag_GetsCompletionForRuleWithParentTag()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("Tag1", "TestAssembly")
                .TagMatchingRule(tagName: "outer-child-tag")
                .TagMatchingRule(tagName: "child-tag", parentTagName: "parent-tag")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("Tag2", "TestAssembly")
                .TagMatchingRule(tagName: "parent-tag")
                .AllowChildTag("child-tag")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["child-tag"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "child-tag",
            containingParentTagName: "parent-tag");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_AllowedChildrenAreIgnoredWhenAtRoot()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("CatchAll", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .AllowChildTag("b")
                .AllowChildTag("bold")
                .AllowChildTag("div")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create([]);
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: null,
            containingParentTagName: null);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_DoesNotReturnExistingCompletionsWhenAllowedChildren()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BoldParent", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .AllowChildTag("b")
                .AllowChildTag("bold")
                .AllowChildTag("div")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["b"] = [],
            ["bold"] = [],
            ["div"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["p", "em"],
            containingTagName: "thing",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_NoneTagHelpers()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BoldParent", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .AllowChildTag("b")
                .AllowChildTag("bold")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["b"] = [],
            ["bold"] = []
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_SomeTagHelpers()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BoldParent", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .AllowChildTag("b")
                .AllowChildTag("bold")
                .AllowChildTag("div")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["b"] = [],
            ["bold"] = [],
            ["div"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_AllTagHelpers()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("BoldParentCatchAll", "TestAssembly")
                .TagMatchingRule(tagName: "*")
                .AllowChildTag("strong")
                .AllowChildTag("div")
                .AllowChildTag("b")
                .Build(),
            TagHelperDescriptorBuilder.CreateTagHelper("BoldParent", "TestAssembly")
                .TagMatchingRule(tagName: "div")
                .AllowChildTag("b")
                .AllowChildTag("bold")
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["strong"] = [tagHelpers[0]],
            ["b"] = [tagHelpers[0]],
            ["bold"] = [tagHelpers[0]],
            ["div"] = [tagHelpers[0], tagHelpers[1]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_MustSatisfyAttributeRules_WithAttributes()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("FormTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "form", static b => b
                    .RequiredAttribute(name: "asp-route-", nameComparison: RequiredAttributeNameComparison.PrefixMatch))
                .Build()
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["form"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["form"],
            containingTagName: "",
            containingParentTagName: "div",
            attributes: [KeyValuePair.Create("asp-route-id", "123")]);
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_MustSatisfyAttributeRules_NoAttributes()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("FormTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "form", static b => b
                    .RequiredAttribute(name: "asp-route-", nameComparison: RequiredAttributeNameComparison.PrefixMatch))
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create([]);
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: ["form"],
            containingTagName: "",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    [Fact]
    public void GetElementCompletions_MustSatisfyAttributeRules_NoAttributes_AllowedIfNotHtml()
    {
        TagHelperCollection tagHelpers =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("ComponentTagHelper", "TestAssembly")
                .TagMatchingRule(tagName: "component", static b => b
                    .RequiredAttribute(name: "type", nameComparison: RequiredAttributeNameComparison.PrefixMatch))
                .Build(),
        ];
 
        var expectedCompletions = ElementCompletionResult.Create(new()
        {
            ["component"] = [tagHelpers[0]]
        });
 
        var completionContext = BuildElementCompletionContext(
            tagHelpers,
            existingCompletions: [],
            containingTagName: "",
            containingParentTagName: "div");
 
        var service = CreateTagHelperCompletionFactsService();
 
        var completions = service.GetElementCompletions(completionContext);
 
        AssertCompletionsAreEquivalent(expectedCompletions, completions);
    }
 
    private static TagHelperCompletionService CreateTagHelperCompletionFactsService() => new();
 
    private static void AssertCompletionsAreEquivalent(ElementCompletionResult expected, ElementCompletionResult actual)
    {
        Assert.Equal(expected.Completions.Count, actual.Completions.Count);
 
        foreach (var (key, value) in expected.Completions)
        {
            var actualValue = actual.Completions[key];
            Assert.NotNull(actualValue);
            Assert.Equal(value, actualValue);
        }
    }
 
    private static void AssertCompletionsAreEquivalent(AttributeCompletionResult expected, AttributeCompletionResult actual)
    {
        Assert.Equal(expected.Completions.Count, actual.Completions.Count);
 
        foreach (var expectedCompletion in expected.Completions)
        {
            var actualValue = actual.Completions[expectedCompletion.Key];
            Assert.NotNull(actualValue);
            Assert.Equal(expectedCompletion.Value, actualValue);
        }
    }
 
    private static ElementCompletionContext BuildElementCompletionContext(
        TagHelperCollection tagHelpers,
        ImmutableArray<string> existingCompletions,
        string? containingTagName,
        string? containingParentTagName = "body",
        bool containingParentIsTagHelper = false,
        string? tagHelperPrefix = null,
        ImmutableArray<KeyValuePair<string, string>> attributes = default)
    {
        attributes = attributes.NullToEmpty();
 
        var documentContext = TagHelperDocumentContext.GetOrCreate(tagHelperPrefix, tagHelpers);
        var completionContext = new ElementCompletionContext(
            documentContext,
            existingCompletions,
            containingTagName,
            attributes,
            containingParentTagName,
            containingParentIsTagHelper,
            inHTMLSchema: static tag => tag is "strong" or "b" or "bold" or "li" or "div" or "form");
 
        return completionContext;
    }
 
    private static AttributeCompletionContext BuildAttributeCompletionContext(
        TagHelperCollection tagHelpers,
        ImmutableArray<string> existingCompletions,
        string currentTagName,
        string? currentAttributeName = null,
        ImmutableArray<KeyValuePair<string, string>> attributes = default,
        string tagHelperPrefix = "")
    {
        attributes = attributes.NullToEmpty();
 
        var documentContext = TagHelperDocumentContext.GetOrCreate(tagHelperPrefix, tagHelpers);
        var completionContext = new AttributeCompletionContext(
            documentContext,
            existingCompletions,
            currentTagName,
            currentAttributeName,
            attributes,
            currentParentTagName: "body",
            currentParentIsTagHelper: false,
            inHTMLSchema: static tag => tag is "strong" or "b" or "bold" or "li" or "div" or "form");
 
        return completionContext;
    }
}