File: ResourceOutgoingPeerResolverTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// 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.Immutable;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Aspire.DashboardService.Proto.V1;
using Aspire.Tests;
using Aspire.Tests.Shared.DashboardModel;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using Struct = Google.Protobuf.WellKnownTypes.Struct;
using Value = Google.Protobuf.WellKnownTypes.Value;
 
namespace Aspire.Dashboard.Tests;
 
public class ResourceOutgoingPeerResolverTests
{
    private static ResourceViewModel CreateResource(string name, string? serviceAddress = null, int? servicePort = null, string? displayName = null, KnownResourceState? state = null)
    {
        return ModelTestHelpers.CreateResource(
            resourceName: name,
            displayName: displayName,
            state: state,
            urls: serviceAddress is null || servicePort is null ? [] : [new UrlViewModel(name, new($"http://{serviceAddress}:{servicePort}"), isInternal: false, isInactive: false, displayProperties: UrlDisplayPropertiesViewModel.Empty)]);
    }
 
    [Fact]
    public void EmptyAttributes_NoMatch()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.False(TryResolvePeerName(resources, [], out _));
    }
 
    [Fact]
    public void EmptyUrlAttribute_NoMatch()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "")], out _));
    }
 
    [Fact]
    public void NullUrlAttribute_NoMatch()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create<string, string>("peer.service", null!)], out _));
    }
 
    [Fact]
    public void ExactValueAttribute_Match()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:5000")], out var value));
        Assert.Equal("test", value);
    }
 
    [Theory]
    [InlineData("127.0.0.1")]
    [InlineData("host.docker.internal")]
    [InlineData("host.containers.internal")]
    [InlineData("HOST.DOCKER.INTERNAL")]
    [InlineData("HOST.CONTAINERS.INTERNAL")]
    public void NonLocalhostAttribute_Match(string host)
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", $"{host}:5000")], out var value));
        Assert.Equal("test", value);
    }
 
    [Fact]
    public void CommaAddressValueAttribute_Match()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "127.0.0.1,5000")], out var value));
        Assert.Equal("test", value);
    }
 
    [Fact]
    public void ServerAddressAndPort_Match()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test"] = CreateResource("test", "localhost", 5000)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "5000")], out var value));
        Assert.Equal("test", value);
    }
 
    [Theory]
    [InlineData("db.namespace", "catalogdb")]
    [InlineData("db.name", "CATALOGDB")]
    public void DatabaseAttributeMatchesDatabaseResourceOverContainerResource(string databaseAttribute, string databaseName)
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["jobsdb"] = CreateResourceWithConnectionString("jobsdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=jobsdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")]),
            ["postgres-evxqcrgg"] = CreateResourceWithConnectionString("postgres-evxqcrgg", "Host=localhost;Port=50267;Username=postgres;Password=password", resourceType: KnownResourceTypes.Container, displayName: "postgres"),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=catalogdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")])
        };
        var attributes = new[]
        {
            KeyValuePair.Create("server.address", "localhost"),
            KeyValuePair.Create("server.port", "50267"),
            KeyValuePair.Create(databaseAttribute, databaseName)
        };
 
        Assert.True(TryResolvePeerName(resources, attributes, out var value));
        Assert.Equal("catalogdb", value);
    }
 
    [Fact]
    public void DatabaseAttributePrefersExactDatabaseNameMatchOverCaseInsensitiveMatch()
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["Catalog"] = CreateResourceWithConnectionString("Catalog", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=Catalog", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")]),
            ["catalog"] = CreateResourceWithConnectionString("catalog", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=catalog", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")])
        };
 
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "50267"), KeyValuePair.Create("db.namespace", "catalog")], out var value));
        Assert.Equal("catalog", value);
    }
 
    [Fact]
    public void DatabaseAttributeMatchesDatabaseResourceAfterAddressTransformationOverExactContainerResource()
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["postgres-evxqcrgg"] = CreateResourceWithConnectionString("postgres-evxqcrgg", "Host=127.0.0.1;Port=50267;Username=postgres;Password=password", resourceType: KnownResourceTypes.Container, displayName: "postgres"),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=catalogdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")])
        };
        var attributes = new[]
        {
            KeyValuePair.Create("server.address", "127.0.0.1"),
            KeyValuePair.Create("server.port", "50267"),
            KeyValuePair.Create("db.namespace", "catalogdb")
        };
 
        Assert.True(TryResolvePeerName(resources, attributes, out var value));
        Assert.Equal("catalogdb", value);
    }
 
    [Theory]
    [InlineData("unknown")]
    [InlineData(null)]
    public void DatabaseAttributeDoesNotMatchDatabaseResource_MatchesContainerResource(string? databaseName)
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["unrelated"] = CreateResource("unrelated", "localhost", 50267),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=catalogdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")]),
            ["postgres-evxqcrgg"] = CreateResourceWithConnectionString("postgres-evxqcrgg", "Host=localhost;Port=50267;Username=postgres;Password=password", resourceType: KnownResourceTypes.Container, displayName: "postgres")
        };
        var attributes = new List<KeyValuePair<string, string>>
        {
            KeyValuePair.Create("server.address", "localhost"),
            KeyValuePair.Create("server.port", "50267")
        };
        if (databaseName is not null)
        {
            attributes.Add(KeyValuePair.Create("db.namespace", databaseName));
        }
 
        Assert.True(TryResolvePeerName(resources, attributes.ToArray(), out var value));
        Assert.Equal("postgres", value);
    }
 
    [Theory]
    [InlineData("unknown")]
    [InlineData(null)]
    public void DatabaseAttributeDoesNotMatchAndNoContainerResource_MatchesFirstDatabaseResource(string? databaseName)
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["unrelated"] = CreateResource("unrelated", "localhost", 50267),
            ["jobsdb"] = CreateResourceWithConnectionString("jobsdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=jobsdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")]),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "Host=localhost;Port=50267;Username=postgres;Password=password;Database=catalogdb", resourceType: "PostgresDatabaseResource", relationships: [new("postgres", "Parent")])
        };
        var attributes = new List<KeyValuePair<string, string>>
        {
            KeyValuePair.Create("server.address", "localhost"),
            KeyValuePair.Create("server.port", "50267")
        };
        if (databaseName is not null)
        {
            attributes.Add(KeyValuePair.Create("db.namespace", databaseName));
        }
 
        Assert.True(TryResolvePeerName(resources, attributes.ToArray(), out var value));
        Assert.Equal("jobsdb", value);
    }
 
    [Fact]
    public void DatabaseAttributeMatchesMongoDbUriResourceOverContainerResource()
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["mongodb"] = CreateResourceWithConnectionString("mongodb", "mongodb://localhost:27017", resourceType: KnownResourceTypes.Container),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "mongodb://localhost:27017/catalogdb", resourceType: "MongoDBDatabaseResource", relationships: [new("mongodb", "Parent")])
        };
 
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "27017"), KeyValuePair.Create("db.namespace", "catalogdb")], out var value));
        Assert.Equal("catalogdb", value);
    }
 
    [Theory]
    [InlineData("DatabaseName")]
    [InlineData("databaseName")]
    public void DatabaseAttributeMatchesDatabaseNameConnectionProperty(string databaseNamePropertyName)
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["jobsdb"] = CreateResourceWithConnectionString("jobsdb", "user id=system;password=password;data source=localhost:1521/FREEPDB1", resourceType: "OracleDatabaseResource", databaseName: "jobsdb", databaseNamePropertyName: databaseNamePropertyName),
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "user id=system;password=password;data source=localhost:1521/FREEPDB1", resourceType: "OracleDatabaseResource", databaseName: "catalogdb", databaseNamePropertyName: databaseNamePropertyName)
        };
 
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("db.namespace", "catalogdb")], out var value));
        Assert.Equal("catalogdb", value);
    }
 
    [Fact]
    public void DatabaseAttributeDoesNotMatchMongoDbUriResource_MatchesContainerResource()
    {
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["catalogdb"] = CreateResourceWithConnectionString("catalogdb", "mongodb://localhost:27017/catalogdb", resourceType: "MongoDBDatabaseResource", relationships: [new("mongodb", "Parent")]),
            ["mongodb"] = CreateResourceWithConnectionString("mongodb", "mongodb://localhost:27017", resourceType: KnownResourceTypes.Container)
        };
 
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "27017"), KeyValuePair.Create("db.namespace", "unknown")], out var value));
        Assert.Equal("mongodb", value);
    }
 
    [Fact]
    public async Task OnPeerChanges_DataUpdates_EventRaised()
    {
        // Arrange
        Assert.Equal(TimeSpan.FromMilliseconds(500), CallbackThrottler.DefaultMinExecuteInterval);
 
        var tcs = new TaskCompletionSource<ResourceViewModelSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        var sourceChannel = Channel.CreateUnbounded<ResourceViewModelChange>();
        var resultChannel = Channel.CreateUnbounded<(int ChangeCount, long Timestamp)>();
        var dashboardClient = new MockDashboardClient(tcs.Task);
        var resolver = CreateResolver(dashboardClient);
        var changeCount = 0;
        resolver.OnPeerChanges(async () =>
        {
            await resultChannel.Writer.WriteAsync((++changeCount, Stopwatch.GetTimestamp()));
        });
 
        Assert.False(resultChannel.Reader.TryRead(out _));
 
        // Act 1
        // Initial resource causes change.
        tcs.SetResult(new ResourceViewModelSubscription(
            [CreateResource("test", serviceAddress: "localhost", servicePort: 8080)],
            GetChanges()));
 
        // Assert 1
        var readValue = await resultChannel.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(1, readValue.ChangeCount);
        var previousTimestamp = readValue.Timestamp;
 
        // Act 2
        // New resource causes change.
        await sourceChannel.Writer.WriteAsync(new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test2", serviceAddress: "localhost", servicePort: 8080, state: KnownResourceState.Starting)));
 
        // Assert 2
        readValue = await resultChannel.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(2, readValue.ChangeCount);
        AssertMinimumExecuteInterval(previousTimestamp, readValue.Timestamp);
        previousTimestamp = readValue.Timestamp;
 
        // Act 3
        // URL change causes change.
        await sourceChannel.Writer.WriteAsync(new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test2", serviceAddress: "localhost", servicePort: 8081, state: KnownResourceState.Starting)));
 
        // Assert 3
        readValue = await resultChannel.Reader.ReadAsync().DefaultTimeout();
        Assert.Equal(3, readValue.ChangeCount);
        AssertMinimumExecuteInterval(previousTimestamp, readValue.Timestamp);
 
        // Act 4
        // Resource update doesn't cause change.
        await sourceChannel.Writer.WriteAsync(new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test2", serviceAddress: "localhost", servicePort: 8081, state: KnownResourceState.Running)));
 
        // Dispose so that we know that all changes are processed.
        await resolver.DisposeAsync().DefaultTimeout();
        resultChannel.Writer.Complete();
 
        // Assert 4
        Assert.False(await resultChannel.Reader.WaitToReadAsync().DefaultTimeout());
        Assert.Equal(3, changeCount);
 
        async IAsyncEnumerable<IReadOnlyList<ResourceViewModelChange>> GetChanges([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await foreach (var item in sourceChannel.Reader.ReadAllAsync(cancellationToken))
            {
                yield return [item];
            }
        }
 
        static void AssertMinimumExecuteInterval(long startTimestamp, long endTimestamp)
        {
            var elapsed = Stopwatch.GetElapsedTime(startTimestamp, endTimestamp);
            Assert.True(elapsed >= CallbackThrottler.DefaultMinExecuteInterval - TimeSpan.FromMilliseconds(50), $"Callbacks executed too quickly: {elapsed}.");
        }
    }
 
    [Fact]
    public async Task Constructor_AmbientActivity_DoesNotFlowToResourceWatch()
    {
        var subscribeResult = new TaskCompletionSource<ResourceViewModelSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        var dashboardClient = new MockDashboardClient(subscribeResult.Task);
        using var activity = new Activity("request").Start();
        var resolver = CreateResolver(dashboardClient);
 
        Assert.Null(await dashboardClient.ActivityOnSubscribe.Task.DefaultTimeout());
 
        var sourceChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
        sourceChannel.Writer.Complete();
        subscribeResult.SetResult(new ResourceViewModelSubscription([], sourceChannel.Reader.ReadAllAsync()));
        await resolver.DisposeAsync().DefaultTimeout();
    }
 
    [Fact]
    public async Task OnPeerChanges_AmbientExecutionContext_DoesNotFlowToCallback()
    {
        var subscribeResult = new TaskCompletionSource<ResourceViewModelSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        var resolver = CreateResolver(new MockDashboardClient(subscribeResult.Task));
        var ambientValue = new AsyncLocal<string?> { Value = "request" };
        var callbackValue = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);
        resolver.OnPeerChanges(() =>
        {
            callbackValue.SetResult(ambientValue.Value);
            return Task.CompletedTask;
        });
 
        var sourceChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
        subscribeResult.SetResult(new ResourceViewModelSubscription(
            [CreateResource("test", serviceAddress: "localhost", servicePort: 8080)],
            sourceChannel.Reader.ReadAllAsync()));
 
        Assert.Null(await callbackValue.Task.DefaultTimeout());
 
        ambientValue.Value = null;
        sourceChannel.Writer.Complete();
        await resolver.DisposeAsync().DefaultTimeout();
    }
 
    [Fact]
    public async Task ResourceChanges_CreateActivityForEachBatch()
    {
        using var activitySource = new DashboardActivitySource();
        var subscribeResult = new TaskCompletionSource<ResourceViewModelSubscription>(TaskCreationOptions.RunContinuationsAsynchronously);
        var sourceChannel = Channel.CreateUnbounded<IReadOnlyList<ResourceViewModelChange>>();
        var activities = new ConcurrentQueue<Activity>();
        var activitiesReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        using var listener = ActivityListenerHelper.Create(activitySource.ActivitySource, onActivityStopped: activity =>
        {
            activities.Enqueue(activity);
            if (activities.Count == 2)
            {
                activitiesReceived.TrySetResult();
            }
        });
        var resolver = CreateResolver(new MockDashboardClient(subscribeResult.Task), activitySource);
        subscribeResult.SetResult(new ResourceViewModelSubscription([], sourceChannel.Reader.ReadAllAsync()));
 
        await sourceChannel.Writer.WriteAsync([new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test", state: KnownResourceState.Starting))]);
        await sourceChannel.Writer.WriteAsync([new ResourceViewModelChange(ResourceViewModelChangeType.Upsert, CreateResource("test", state: KnownResourceState.Running))]);
        await activitiesReceived.Task.DefaultTimeout();
        sourceChannel.Writer.Complete();
        await resolver.DisposeAsync().DefaultTimeout();
 
        Assert.Collection(
            activities,
            AssertActivity,
            AssertActivity);
 
        static void AssertActivity(Activity activity)
        {
            Assert.Equal(DashboardActivitySource.ActivitySourceName, activity.Source.Name);
            Assert.Equal("Process resource subscription changes", activity.OperationName);
            Assert.Equal(ActivityKind.Consumer, activity.Kind);
        }
    }
 
    [Fact]
    public void NameAndDisplayNameDifferent_OneInstance_ReturnDisplayName()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test-abc"] = CreateResource("test-abc", "localhost", 5000, displayName: "test")
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "5000")], out var value));
        Assert.Equal("test", value);
    }
 
    [Fact]
    public void NameAndDisplayNameDifferent_MultipleInstances_ReturnName()
    {
        // Arrange
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["test-abc"] = CreateResource("test-abc", "localhost", 5000, displayName: "test"),
            ["test-def"] = CreateResource("test-def", "localhost", 5001, displayName: "test")
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "5000")], out var value1));
        Assert.Equal("test-abc", value1);
 
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "localhost"), KeyValuePair.Create("server.port", "5001")], out var value2));
        Assert.Equal("test-def", value2);
    }
 
    private static bool TryResolvePeerName(IDictionary<string, ResourceViewModel> resources, KeyValuePair<string, string>[] attributes, out string? peerName)
    {
        return ResourceOutgoingPeerResolver.TryResolvePeerCore(resources, attributes, out peerName, out _);
    }
 
    private static ResourceOutgoingPeerResolver CreateResolver(IResourceRepository resourceRepository, DashboardActivitySource? activitySource = null)
    {
        return new ResourceOutgoingPeerResolver(
            resourceRepository,
            activitySource ?? new DashboardActivitySource(),
            NullLogger<ResourceOutgoingPeerResolver>.Instance);
    }
 
    [Fact]
    public void ConnectionStringWithEndpoint_Match()
    {
        // Arrange - GitHub Models resource with connection string containing endpoint
        var connectionString = "Endpoint=https://models.github.ai/inference;Key=test-key;Model=openai/gpt-4o-mini";
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["github-model"] = CreateResourceWithConnectionString("github-model", connectionString)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "models.github.ai:443")], out var value));
        Assert.Equal("github-model", value);
    }
 
    [Fact]
    public void ConnectionStringWithEndpointOrganization_Match()
    {
        // Arrange - GitHub Models resource with organization endpoint
        var connectionString = "Endpoint=https://models.github.ai/orgs/myorg/inference;Key=test-key;Model=openai/gpt-4o-mini";
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["github-model"] = CreateResourceWithConnectionString("github-model", connectionString)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "models.github.ai:443")], out var value));
        Assert.Equal("github-model", value);
    }
 
    [Fact]
    public void ParameterWithUrlValue_Match()
    {
        // Arrange - Parameter resource with URL value
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["api-url-param"] = CreateResourceWithParameterValue("api-url-param", "https://api.example.com:8080/endpoint")
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "api.example.com:8080")], out var value));
        Assert.Equal("api-url-param", value);
    }
 
    [Fact]
    public void ConnectionStringWithoutEndpoint_NoMatch()
    {
        // Arrange - Connection string without Endpoint property
        var connectionString = "Server=localhost;Database=test;User=admin;Password=secret";
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["sql-connection"] = CreateResourceWithConnectionString("sql-connection", connectionString)
        };
 
        // Act & Assert
        Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:1433")], out _));
    }
 
    [Fact]
    public void ParameterWithNonUrlValue_NoMatch()
    {
        // Arrange - Parameter resource with non-URL value
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["config-param"] = CreateResourceWithParameterValue("config-param", "simple-config-value")
        };
 
        // Act & Assert
        Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:5000")], out _));
    }
 
    [Fact]
    public void ConnectionStringAsDirectUrl_Match()
    {
        // Arrange - Connection string that is itself a URL (e.g., blob storage)
        var connectionString = "https://mystorageaccount.blob.core.windows.net/";
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["blob-storage"] = CreateResourceWithConnectionString("blob-storage", connectionString)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "mystorageaccount.blob.core.windows.net:443")], out var value));
        Assert.Equal("blob-storage", value);
    }
 
    [Fact]
    public void ConnectionStringAsDirectUrlWithCustomPort_Match()
    {
        // Arrange - Connection string that is itself a URL with custom port
        var connectionString = "https://myvault.vault.azure.net:8080/";
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["key-vault"] = CreateResourceWithConnectionString("key-vault", connectionString)
        };
 
        // Act & Assert
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "myvault.vault.azure.net:8080")], out var value));
        Assert.Equal("key-vault", value);
    }
 
    private static ResourceViewModel CreateResourceWithConnectionString(string name, string connectionString, string resourceType = KnownResourceTypes.ConnectionString, string? displayName = null, ImmutableArray<RelationshipViewModel>? relationships = null, string? databaseName = null, string databaseNamePropertyName = "DatabaseName")
    {
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Resource.ConnectionString] = new(
                name: KnownProperties.Resource.ConnectionString,
                value: Value.ForString(connectionString),
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: 0,
                displayName: null,
                isHighlighted: false)
        };
 
        if (databaseName is not null)
        {
            var connectionProperties = new Struct();
            connectionProperties.Fields[databaseNamePropertyName] = Value.ForString(databaseName);
            properties[KnownProperties.Resource.ConnectionProperties] = new(
                name: KnownProperties.Resource.ConnectionProperties,
                value: new Value { StructValue = connectionProperties },
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: 0,
                displayName: null,
                isHighlighted: false);
        }
 
        return ModelTestHelpers.CreateResource(
            resourceName: name,
            displayName: displayName,
            resourceType: resourceType,
            relationships: relationships,
            properties: properties);
    }
 
    private static ResourceViewModel CreateResourceWithParameterValue(string name, string value)
    {
        var properties = new Dictionary<string, ResourcePropertyViewModel>
        {
            [KnownProperties.Parameter.Value] = new(
                name: KnownProperties.Parameter.Value,
                value: Value.ForString(value),
                isValueSensitive: false,
                knownProperty: null,
                sortOrder: 0,
                displayName: null,
                isHighlighted: false)
        };
 
        return ModelTestHelpers.CreateResource(
            resourceName: name,
            resourceType: KnownResourceTypes.Parameter,
            properties: properties);
    }
 
    [Fact]
    public void MultipleResourcesMatch_ViaTransformation_ReturnsFirstMatch()
    {
        // Arrange - Resources that match via address transformation
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["sql-primary"] = CreateResource("sql-primary", "localhost", 1433),
            ["sql-replica"] = CreateResource("sql-replica", "127.0.0.1", 1433)
        };
 
        // Act & Assert - Should return the first match found
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "127.0.0.1:1433")], out var name));
        Assert.Equal("sql-replica", name);
    }
 
    [Fact]
    public void MultipleResourcesSameAddress_ReturnsFirstMatch()
    {
        // Test to verify that "first one wins" logic is restored
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["database1"] = CreateResource("database1", "localhost", 5432),
            ["database2"] = CreateResource("database2", "localhost", 5432)
        };
 
        // Should return the first match found (verifies regression fix)
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:5432")], out var name));
        Assert.NotNull(name);
        // Should match one of the databases (first one found)
        Assert.Contains(name, new[] { "database1", "database2" });
    }
 
    [Fact]
    public void SingleResourceAfterTransformation_ReturnsTrue()
    {
        // Arrange - Only one resource that matches after address transformation
        var resources = new Dictionary<string, ResourceViewModel>
        {
            ["unique-service"] = CreateResource("unique-service", "localhost", 8080),
            ["other-service"] = CreateResource("other-service", "remotehost", 9090)
        };
 
        // Act & Assert - Only the first resource should match "127.0.0.1:8080" after transformation
        Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "127.0.0.1:8080")], out var name));
        Assert.Equal("unique-service", name);
    }
 
    private sealed class MockDashboardClient(Task<ResourceViewModelSubscription> subscribeResult) : IDashboardClient
    {
        public TaskCompletionSource<Activity?> ActivityOnSubscribe { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public bool IsEnabled => true;
        public Task WhenConnected => Task.CompletedTask;
        public string ApplicationName => "ApplicationName";
        public string? MinRequiredVersion => null;
        public DashboardConnectionState ConnectionState => DashboardConnectionState.Connected;
#pragma warning disable CS0067 // Event is never used - required by interface
        public event Action<DashboardConnectionState>? ConnectionStateChanged;
#pragma warning restore CS0067
        public Task ReconnectAsync() => Task.CompletedTask;
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
        public Task<ResourceCommandResponseViewModel> ExecuteResourceCommandAsync(string resourceName, string resourceType, CommandViewModel command, ExecuteResourceCommandOptions options, CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task<string> UploadFileAsync(Stream fileStream, string fileName, long expectedSize, int interactionId, string inputName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public ResourceViewModel? GetResource(string resourceName) => null;
        public IReadOnlyList<ResourceViewModel> GetResources() => [];
        public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> GetConsoleLogs(string resourceName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task ClearConsoleLogsAsync(IReadOnlyList<string> resourceNames, DateTime clearDate) => Task.CompletedTask;
        public Task SendInteractionRequestAsync(WatchInteractionsRequestUpdate request, CancellationToken cancellationToken) => throw new NotImplementedException();
        public IAsyncEnumerable<IReadOnlyList<ResourceLogLine>> SubscribeConsoleLogs(string resourceName, CancellationToken cancellationToken) => throw new NotImplementedException();
        public IAsyncEnumerable<WatchInteractionsResponseUpdate> SubscribeInteractionsAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
        public Task<ResourceViewModelSubscription> SubscribeResourcesAsync(CancellationToken cancellationToken)
        {
            ActivityOnSubscribe.TrySetResult(Activity.Current);
            return subscribeResult;
        }
    }
}