File: AzureBicepProvisionerTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREPIPELINES002
#pragma warning disable ASPIREFILESYSTEM001
 
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Provisioning;
using Aspire.Hosting.Azure.Provisioning.Internal;
using Aspire.Hosting.Azure.Resources;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Tests;
using Aspire.Hosting.Utils;
using Azure;
using Azure.Core;
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;
using Azure.ResourceManager.Resources.Models;
using Azure.Security.KeyVault.Secrets;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureBicepProvisionerTests
{
    [Theory]
    [InlineData("1alpha")]
    [InlineData("-alpha")]
    [InlineData("")]
    [InlineData(" alpha")]
    [InlineData("alpha 123")]
    public void WithParameterDoesNotAllowParameterNamesWhichAreInvalidBicepIdentifiers(string bicepParameterName)
    {
        Assert.Throws<ArgumentException>(() =>
        {
            using var builder = TestDistributedApplicationBuilder.Create();
            builder.AddAzureInfrastructure("infrastructure", _ => { })
                   .WithParameter(bicepParameterName);
        });
    }
 
    [Theory]
    [InlineData("alpha")]
    [InlineData("a1pha")]
    [InlineData("_alpha")]
    [InlineData("__alpha")]
    [InlineData("alpha1_")]
    [InlineData("Alpha1_A")]
    public void WithParameterAllowsParameterNamesWhichAreValidBicepIdentifiers(string bicepParameterName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.AddAzureInfrastructure("infrastructure", _ => { })
                .WithParameter(bicepParameterName);
    }
 
    [Fact]
    public async Task NestedChildResourcesShouldGetUpdated()
    {
        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
 
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var cosmos = builder.AddAzureCosmosDB("cosmosdb");
        var db = cosmos.AddCosmosDatabase("db");
        var entries = db.AddContainer("entries", "/id");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        await app.StartAsync(cts.Token);
 
        var rns = app.Services.GetRequiredService<ResourceNotificationService>();
        await foreach (var resourceEvent in rns.WatchAsync(cts.Token).WithCancellation(cts.Token))
        {
            if (resourceEvent.Resource == entries.Resource)
            {
                var parentProperty = resourceEvent.Snapshot.Properties.FirstOrDefault(x => x.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
                Assert.Equal("db", parentProperty);
                return;
            }
        }
 
        Assert.Fail();
    }
 
    [Fact]
    public void BicepProvisioner_CanBeInstantiated()
    {
        // Test that BicepProvisioner can be instantiated with required dependencies
 
        // Arrange
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        var services = builder.Services.BuildServiceProvider();
 
        var bicepExecutor = new TestBicepCliExecutor();
        var secretClientProvider = new TestSecretClientProvider();
        var tokenCredentialProvider = new TestTokenCredentialProvider();
 
        // Act
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            bicepExecutor,
            secretClientProvider,
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        // Assert
        Assert.NotNull(provisioner);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_InPublishMode_ThrowsForUnknownPrincipalParameters()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage-roles", templateString: "output id string = 'ok'");
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalId] = null;
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalName] = null;
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType] = null;
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Contains("Azure principal parameter was not supplied", exception.Message);
    }
 
    [Theory]
    [InlineData("User")]
    [InlineData("ServicePrincipal")]
    public async Task GetOrCreateResourceAsync_InPublishMode_PopulatesUserPrincipalParameters(string principalType)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("sandbox-group", templateString: "output id string = 'ok'");
        resource.Parameters[AzureBicepResource.KnownParameters.UserPrincipalId] = null;
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType] = null;
 
        var sentinel = new InvalidOperationException("stop-after-populate-well-known-parameters");
        var bicepExecutor = new ThrowingBicepCompiler(sentinel);
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            bicepExecutor,
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var principalId = Guid.Parse("11111111-2222-3333-4444-555555555555");
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            principal: new AzurePrincipal(principalId, "deployment-principal", principalType),
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        var thrown = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
        Assert.Same(sentinel, thrown);
 
        Assert.Equal(principalId, resource.Parameters[AzureBicepResource.KnownParameters.UserPrincipalId]);
        Assert.Equal(principalType, resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType]);
    }
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run)]
    [InlineData(DistributedApplicationOperation.Publish)]
    public async Task GetOrCreateResourceAsync_ThrowsWhenExplicitUserPrincipalIdHasNoPrincipalType(DistributedApplicationOperation operation)
    {
        using var builder = TestDistributedApplicationBuilder.Create(operation);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("sandbox-group", templateString: "output id string = 'ok'");
        resource.Parameters[AzureBicepResource.KnownParameters.UserPrincipalId] = Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType] = null;
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(operation),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            executionContext: new DistributedApplicationExecutionContext(operation));
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(
            "The Azure parameter 'principalType' must be supplied when 'userPrincipalId' is provided explicitly.",
            exception.Message);
    }
 
    [Theory]
    [InlineData("User")]
    [InlineData("ServicePrincipal")]
    [InlineData("Group")]
    public async Task GetOrCreateResourceAsync_InRunMode_PopulatesPrincipalTypeFromContext(string principalType)
    {
        // Regression test for https://github.com/microsoft/aspire/issues/13933.
        // The PrincipalType value must come from the credential's detected principal type
        // (carried on ProvisioningContext.Principal.Type) instead of a hardcoded "User",
        // otherwise role-assignment Bicep deployments fail under service-principal /
        // federated workload identity credentials with PrincipalNotFound / UnmatchedPrincipalType.
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage-roles", templateString: "output id string = 'ok'");
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalId] = null;
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalName] = null;
        resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType] = null;
 
        // The compiler stub raises a sentinel so the flow stops right after
        // PopulateWellKnownParameters mutates the resource. The downstream ARM deployment
        // requires a live Azure connection, so short-circuiting here keeps the test hermetic.
        var sentinel = new InvalidOperationException("stop-after-populate-well-known-parameters");
        var bicepExecutor = new ThrowingBicepCompiler(sentinel);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            bicepExecutor,
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            principal: new AzurePrincipal(Guid.Parse("11111111-2222-3333-4444-555555555555"), "ci-runner", principalType));
 
        var thrown = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
        Assert.Same(sentinel, thrown);
 
        Assert.Equal(principalType, resource.Parameters[AzureBicepResource.KnownParameters.PrincipalType]);
    }
 
    private sealed class ThrowingBicepCompiler(Exception toThrow) : IBicepCompiler
    {
        public Task<string> CompileBicepToArmAsync(string bicepFilePath, CancellationToken cancellationToken = default)
        {
            throw toThrow;
        }
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_WithSubscriptionScope_UsesSubscriptionDeploymentCollection()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var subscription = new TestSubscriptionResource([], subscriptionId: "00000000-0000-0000-0000-000000000042");
        var tenant = new TestTenantResource([]);
        var deploymentResourceGroup = new TestResourceGroupResource("deploy-rg");
        var resource = new AzureBicepResource("subscriptionScoped", templateString: """
            targetScope = 'subscription'
            output result string = 'ok'
            """);
        resource.Scope = AzureBicepResourceScope.CreateForSubscription(subscription.Id.Name);
 
        var provisioner = CreateProvisioner(services);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(subscription, tenant),
            subscription: subscription,
            resourceGroup: deploymentResourceGroup,
            tenant: tenant,
            location: AzureLocation.WestUS2,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.True(subscription.Deployments.WasCreateOrUpdateCalled);
        Assert.False(deploymentResourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.False(tenant.Deployments.WasCreateOrUpdateCalled);
        Assert.Equal(AzureLocation.WestUS2.Name, subscription.Deployments.Content!.Location?.Name);
        Assert.StartsWith("subscriptionScoped-", subscription.Deployments.DeploymentName);
        AssertResourceGroupPropertyIsNull(services, resource.Name);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_WithTenantScope_UsesTenantDeploymentCollection()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var subscription = new TestSubscriptionResource([]);
        var tenant = new TestTenantResource([]);
        var deploymentResourceGroup = new TestResourceGroupResource("deploy-rg");
        var resource = new AzureBicepResource("tenantScoped", templateString: """
            targetScope = 'tenant'
            output result string = 'ok'
            """);
        resource.Scope = AzureBicepResourceScope.CreateForTenant();
 
        var provisioner = CreateProvisioner(services);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(subscription, tenant),
            subscription: subscription,
            resourceGroup: deploymentResourceGroup,
            tenant: tenant,
            location: AzureLocation.WestUS2,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.True(tenant.Deployments.WasCreateOrUpdateCalled);
        Assert.False(subscription.Deployments.WasCreateOrUpdateCalled);
        Assert.False(deploymentResourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.Equal(AzureLocation.WestUS2.Name, tenant.Deployments.Content!.Location?.Name);
        Assert.StartsWith("tenantScoped-", tenant.Deployments.DeploymentName);
        AssertResourceGroupPropertyIsNull(services, resource.Name);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_WithResourceGroupAndSubscriptionScope_UsesScopedResourceGroupDeploymentCollection()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var scopedSubscriptionId = "00000000-0000-0000-0000-000000000043";
        var existingResourceGroup = new TestResourceGroupResource("existing-rg", [], scopedSubscriptionId);
        var scopedSubscription = new TestSubscriptionResource([], existingResourceGroup, scopedSubscriptionId);
        var deploymentResourceGroup = new TestResourceGroupResource("deploy-rg");
        var tenant = new TestTenantResource([]);
        var resource = new AzureBicepResource("existingbus", templateString: "output result string = 'ok'");
        resource.Scope = new AzureBicepResourceScope(existingResourceGroup.Name, scopedSubscriptionId);
 
        var provisioner = CreateProvisioner(services);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(scopedSubscription, tenant),
            subscription: new TestSubscriptionResource([]),
            resourceGroup: deploymentResourceGroup,
            tenant: tenant,
            location: AzureLocation.WestUS2,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.Equal(existingResourceGroup.Name, scopedSubscription.ResourceGroups.LastRequestedResourceGroupName);
        Assert.True(existingResourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.False(scopedSubscription.Deployments.WasCreateOrUpdateCalled);
        Assert.False(deploymentResourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.Null(existingResourceGroup.Deployments.Content!.Location);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_WithDefaultScope_UsesResourceGroupDeploymentCollection()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var subscription = new TestSubscriptionResource([]);
        var tenant = new TestTenantResource([]);
        var resourceGroup = new TestResourceGroupResource("deploy-rg");
        var resource = new AzureBicepResource("defaultScoped", templateString: "output result string = 'ok'");
 
        var provisioner = CreateProvisioner(services);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(subscription, tenant),
            subscription: subscription,
            resourceGroup: resourceGroup,
            tenant: tenant,
            location: AzureLocation.WestUS2,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.True(resourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.False(subscription.Deployments.WasCreateOrUpdateCalled);
        Assert.False(tenant.Deployments.WasCreateOrUpdateCalled);
        Assert.Null(resourceGroup.Deployments.Content!.Location);
        Assert.StartsWith("defaultScoped-", resourceGroup.Deployments.DeploymentName);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_WithSubscriptionScopeInRunMode_UsesSubscriptionDeploymentCollection()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var subscription = new TestSubscriptionResource([], subscriptionId: "00000000-0000-0000-0000-000000000044");
        var tenant = new TestTenantResource([]);
        var deploymentResourceGroup = new TestResourceGroupResource("deploy-rg");
        var resource = new AzureBicepResource("subscriptionScoped", templateString: """
            targetScope = 'subscription'
            output result string = 'ok'
            """);
        resource.Scope = AzureBicepResourceScope.CreateForSubscription(subscription.Id.Name);
 
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(subscription, tenant),
            subscription: subscription,
            resourceGroup: deploymentResourceGroup,
            tenant: tenant,
            location: AzureLocation.WestUS2,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.True(subscription.Deployments.WasCreateOrUpdateCalled);
        Assert.False(deploymentResourceGroup.Deployments.WasCreateOrUpdateCalled);
        Assert.False(tenant.Deployments.WasCreateOrUpdateCalled);
        Assert.Equal(AzureLocation.WestUS2.Name, subscription.Deployments.Content!.Location?.Name);
        Assert.Equal("subscriptionScoped", subscription.Deployments.DeploymentName);
        AssertResourceGroupPropertyIsNull(services, resource.Name);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_InPublishMode_DoesNotQueryDeploymentOperationsAfterSuccessfulDeployment()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage", templateString: "output name string = 'storage'");
        var armClient = new TestArmClient(
        [
            new AzureDeploymentOperationDetails(
                OperationId: "storage-create",
                DeploymentId: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage",
                ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                ProvisioningState: AzureDeploymentOperationDetails.SucceededState,
                Timestamp: null,
                Duration: null,
                StatusCode: "OK",
                ServiceRequestId: null,
                TargetResource: new AzureDeploymentOperationTarget(
                    "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage",
                    "Microsoft.Storage/storageAccounts",
                    "storage"),
                FailureDetails: null)
        ]);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.Equal(0, armClient.DeploymentOperationsCallCount);
        Assert.Equal(0, armClient.SupportedLocationsCallCount);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_InPublishMode_EnrichesDeploymentStartFailures()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("search", templateString: "output name string = 'search'");
        resource.Parameters[AzureBicepResource.KnownParameters.Location] = "australiacentral";
        var armClient = new TestArmClient(
            [],
            new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase)
            {
                ["Microsoft.Search/searchServices"] = ["australiaeast", "westus3"]
            });
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            resourceGroup: new DeploymentCollectionResourceGroupResource(new LocationUnavailableArmDeploymentCollection()),
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        var exception = await Assert.ThrowsAsync<AzureProvisioningFailureException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, exception.FailureDetails.ErrorCode);
        Assert.Equal("Microsoft.Search/searchServices", exception.FailureDetails.ResourceType);
        Assert.Equal("australiacentral", exception.FailureDetails.CurrentLocation);
        Assert.Equal(["australiaeast", "westus3"], exception.FailureDetails.SupportedLocations);
        Assert.DoesNotContain(exception.FailureDetails.RecommendedActions, static action => action.Code == "change-location");
        Assert.Contains(exception.FailureDetails.RecommendedActions, static action => action.Code == "set-location");
        Assert.Contains("Azure__Location", exception.Message, StringComparison.Ordinal);
        Assert.Equal(0, armClient.DeploymentOperationsCallCount);
        Assert.Equal(1, armClient.SupportedLocationsCallCount);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_InPublishMode_UsesDeploymentOperationDetailsWhenWaitingFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("search", templateString: "output name string = 'search'");
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new WaitingThrowingArmDeploymentCollection());
        var deploymentId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/search-0";
        var failedSearchId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/search";
        var failureDetails = new AzureProvisioningFailureDetails(
            Provider: "Microsoft.Search",
            ResourceType: "Microsoft.Search/searchServices",
            ResourceName: "search",
            TargetResourceId: failedSearchId,
            CurrentLocation: null,
            SupportedLocations: [],
            HttpStatus: 400,
            ErrorCode: AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode,
            ErrorMessage: "The provided location 'antarctica' is not available for resource type 'Microsoft.Search/searchServices'.",
            Operation: "deploy",
            RequestId: "request-id",
            CorrelationId: "correlation-id",
            RecommendedActions: AzureProvisioningFailureDetails.GetRecommendedActions(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode));
        var armClient = new TestArmClient(
            [
                new AzureDeploymentOperationDetails(
                    OperationId: "search-create",
                    DeploymentId: deploymentId,
                    ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                    ProvisioningState: AzureDeploymentOperationDetails.FailedState,
                    Timestamp: DateTimeOffset.Parse("2026-06-11T06:39:22Z"),
                    Duration: TimeSpan.FromSeconds(15),
                    StatusCode: "BadRequest",
                    ServiceRequestId: "service-request-id",
                    TargetResource: new AzureDeploymentOperationTarget(failedSearchId, "Microsoft.Search/searchServices", "search"),
                    FailureDetails: failureDetails)
            ],
            new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase)
            {
                ["Microsoft.Search/searchServices"] = ["eastus", "westus3"]
            });
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            resourceGroup: resourceGroup,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        var exception = await Assert.ThrowsAsync<AzureProvisioningFailureException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, exception.FailureDetails.ErrorCode);
        Assert.Equal("Microsoft.Search/searchServices", exception.FailureDetails.ResourceType);
        Assert.Equal(failedSearchId, exception.FailureDetails.TargetResourceId);
        Assert.Equal("westus2", exception.FailureDetails.CurrentLocation);
        Assert.Equal(["eastus", "westus3"], exception.FailureDetails.SupportedLocations);
        Assert.Collection(
            exception.FailureDetails.RecommendedActions,
            action =>
            {
                Assert.Equal("set-location", action.Code);
                Assert.Contains("eastus", action.Message, StringComparison.Ordinal);
            },
            action => Assert.Equal("clear-deployment-cache", action.Code));
        Assert.Contains("Azure__Location", exception.Message, StringComparison.Ordinal);
        Assert.Equal(1, armClient.DeploymentOperationsCallCount);
        Assert.Equal(1, armClient.SupportedLocationsCallCount);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_InPublishMode_EnrichesDeploymentOperationFailuresInParallel()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.Services.AddSingleton<IDeploymentStateManager>(new MockDeploymentStateManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("search", templateString: "output name string = 'search'");
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new WaitingThrowingArmDeploymentCollection());
        var deploymentId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/search-0";
        var failedSearchId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/search";
        var failedCosmosId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/cosmos";
        var allLookupsStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var activeLookups = 0;
        var startedLookups = 0;
        var maxConcurrentLookups = 0;
        var armClient = new TestArmClient(
            [
                CreateLocationFailureOperation(deploymentId, "search-create", failedSearchId, "Microsoft.Search/searchServices", "search"),
                CreateLocationFailureOperation(deploymentId, "cosmos-create", failedCosmosId, "Microsoft.DocumentDB/databaseAccounts", "cosmos")
            ],
            supportedLocationsProvider: async (subscriptionId, resourceType, cancellationToken) =>
            {
                _ = subscriptionId;
                var active = Interlocked.Increment(ref activeLookups);
                try
                {
                    UpdateMaxConcurrentLookups(active);
 
                    if (Interlocked.Increment(ref startedLookups) == 2)
                    {
                        allLookupsStarted.TrySetResult();
                    }
 
                    await allLookupsStarted.Task.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
 
                    return resourceType.Contains("Search", StringComparison.OrdinalIgnoreCase)
                        ? ["eastus"]
                        : ["westus3"];
                }
                finally
                {
                    Interlocked.Decrement(ref activeLookups);
                }
            });
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            resourceGroup: resourceGroup,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
 
        await Assert.ThrowsAsync<AzureProvisioningFailureException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(2, armClient.SupportedLocationsCallCount);
        Assert.Equal(2, Volatile.Read(ref maxConcurrentLookups));
 
        void UpdateMaxConcurrentLookups(int active)
        {
            while (true)
            {
                var observed = Volatile.Read(ref maxConcurrentLookups);
                if (active <= observed ||
                    Interlocked.CompareExchange(ref maxConcurrentLookups, active, observed) == observed)
                {
                    return;
                }
            }
        }
 
        static AzureDeploymentOperationDetails CreateLocationFailureOperation(
            string deploymentId,
            string operationId,
            string targetResourceId,
            string resourceType,
            string resourceName)
        {
            return new(
                OperationId: operationId,
                DeploymentId: deploymentId,
                ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                ProvisioningState: AzureDeploymentOperationDetails.FailedState,
                Timestamp: DateTimeOffset.Parse("2026-06-11T06:39:22Z"),
                Duration: TimeSpan.FromSeconds(15),
                StatusCode: "BadRequest",
                ServiceRequestId: "service-request-id",
                TargetResource: new AzureDeploymentOperationTarget(targetResourceId, resourceType, resourceName),
                FailureDetails: new AzureProvisioningFailureDetails(
                    Provider: resourceType[..resourceType.IndexOf('/', StringComparison.Ordinal)],
                    ResourceType: resourceType,
                    ResourceName: resourceName,
                    TargetResourceId: targetResourceId,
                    CurrentLocation: null,
                    SupportedLocations: [],
                    HttpStatus: 400,
                    ErrorCode: AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode,
                    ErrorMessage: $"The provided location 'antarctica' is not available for resource type '{resourceType}'.",
                    Operation: "deploy",
                    RequestId: "request-id",
                    CorrelationId: "correlation-id",
                    RecommendedActions: AzureProvisioningFailureDetails.GetRecommendedActions(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode)));
        }
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_UsesEffectiveResourceLocationInSnapshot()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage", templateString: "output name string = 'storage'");
        resource.Parameters[AzureBicepResource.KnownParameters.Location] = "westus3";
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(location: AzureLocation.WestUS2);
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        AssertHighlightedContextProperty(resourceEvent.Snapshot.Properties, "azure.subscription.id", "12345678-1234-1234-1234-123456789012", AzureProvisioningStrings.ContextPropertySubscriptionIdDisplayName);
        AssertHighlightedContextProperty(resourceEvent.Snapshot.Properties, "azure.resource.group", "test-rg", AzureProvisioningStrings.ContextPropertyResourceGroupDisplayName);
        AssertHighlightedContextProperty(resourceEvent.Snapshot.Properties, "azure.tenant.id", "87654321-4321-4321-4321-210987654321", AzureProvisioningStrings.ContextPropertyTenantIdDisplayName);
        AssertHighlightedContextProperty(resourceEvent.Snapshot.Properties, "azure.tenant.domain", "testdomain.onmicrosoft.com", AzureProvisioningStrings.ContextPropertyTenantDomainDisplayName);
        AssertHighlightedContextProperty(resourceEvent.Snapshot.Properties, "azure.location", "westus3", AzureProvisioningStrings.ContextPropertyLocationDisplayName);
        Assert.Equal("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage", resourceEvent.Snapshot.Properties.Single(p => p.Name == CustomResourceKnownProperties.Source).Value?.ToString());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_PublishesPredictedDeploymentIdBeforeDeploymentStarts()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var resourceGroup = new ThrowingResourceGroupResource("test-rg");
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<RequestFailedException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("87654321-4321-4321-4321-210987654321", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.tenant.id").Value?.ToString());
        Assert.Equal("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage2", resourceEvent.Snapshot.Properties.Single(p => p.Name == CustomResourceKnownProperties.Source).Value?.ToString());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_PublishesSubscriptionScopedPredictedDeploymentIdAndUrlWhileWaiting()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var subscription = new WaitingThrowingSubscriptionResource();
        var tenant = new TestTenantResource();
        var resource = new AzureBicepResource("subscriptionDeployment", templateString: "output name string = 'subscriptionDeployment'")
        {
            Scope = AzureBicepResourceScope.CreateForSubscription(subscription.Id.Name)
        };
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(subscription, tenant),
            subscription: subscription,
            tenant: tenant);
 
        await Assert.ThrowsAsync<RequestFailedException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        var expectedDeploymentId = "/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Resources/deployments/subscriptionDeployment";
        Assert.Equal("87654321-4321-4321-4321-210987654321", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.tenant.id").Value?.ToString());
        Assert.Equal(expectedDeploymentId, resourceEvent.Snapshot.Properties.Single(p => p.Name == CustomResourceKnownProperties.Source).Value?.ToString());
        Assert.Equal(BicepProvisioner.GetDeploymentUrl(new ResourceIdentifier(expectedDeploymentId)), resourceEvent.Snapshot.Urls.Single(u => u.Name == "deployment").Url);
    }
 
    [Fact]
    public async Task ConfigureResourceAsync_DoesNotReuseOverrideOnlyDeploymentState()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var reused = await provisioner.ConfigureResourceAsync(resource, CancellationToken.None);
 
        Assert.False(reused);
        Assert.Empty(resource.Outputs);
    }
 
    [Fact]
    public async Task ConfigureResourceAsync_DoesNotReuseInProgressDeploymentState()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var parameters = new JsonObject();
        var checksum = BicepUtilities.GetChecksum(resource, parameters, scope: null);
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data["Id"] = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage2";
        section.Data["Parameters"] = parameters.ToJsonString();
        section.Data["Outputs"] = """{"name":{"value":"storage2"}}""";
        section.Data["CheckSum"] = checksum;
        section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var reused = await provisioner.ConfigureResourceAsync(resource, CancellationToken.None);
 
        Assert.False(reused);
        Assert.Empty(resource.Outputs);
    }
 
    [Fact]
    public async Task ConfigureResourceAsync_PublishesAzureIdentityPropertiesFromDeploymentState()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var parameters = new JsonObject();
        var checksum = BicepUtilities.GetChecksum(resource, parameters, scope: null);
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure", CancellationToken.None);
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["ResourceGroup"] = "test-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        azureSection.Data["Tenant"] = "microsoft.onmicrosoft.com";
        azureSection.Data["Location"] = "westus2";
        await deploymentStateManager.SaveSectionAsync(azureSection, CancellationToken.None);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data["Id"] = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage2";
        section.Data["Parameters"] = parameters.ToJsonString();
        section.Data["Outputs"] = """{"name":{"value":"storage2"}}""";
        section.Data["CheckSum"] = checksum;
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var reused = await provisioner.ConfigureResourceAsync(resource, CancellationToken.None);
 
        Assert.True(reused);
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("12345678-1234-1234-1234-123456789012", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.subscription.id").Value?.ToString());
        Assert.Equal("test-rg", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.resource.group").Value?.ToString());
        Assert.Equal("87654321-4321-4321-4321-210987654321", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.tenant.id").Value?.ToString());
        Assert.Equal("microsoft.onmicrosoft.com", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.tenant.domain").Value?.ToString());
        Assert.Equal("westus2", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.location").Value?.ToString());
        Assert.Equal("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage2", resourceEvent.Snapshot.Properties.Single(p => p.Name == CustomResourceKnownProperties.Source).Value?.ToString());
    }
 
    [Fact]
    public async Task ConfigureResourceAsync_PublishesResourceGroupFromCachedDeploymentId()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var parameters = new JsonObject();
        var checksum = BicepUtilities.GetChecksum(resource, parameters, scope: null);
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure", CancellationToken.None);
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["ResourceGroup"] = "environment-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        azureSection.Data["Tenant"] = "microsoft.onmicrosoft.com";
        azureSection.Data["Location"] = "westus2";
        await deploymentStateManager.SaveSectionAsync(azureSection, CancellationToken.None);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data["Id"] = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/scoped-rg/providers/Microsoft.Resources/deployments/storage2";
        section.Data["Parameters"] = parameters.ToJsonString();
        section.Data["Outputs"] = """{"name":{"value":"storage2"}}""";
        section.Data["CheckSum"] = checksum;
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var reused = await provisioner.ConfigureResourceAsync(resource, CancellationToken.None);
 
        Assert.True(reused);
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("scoped-rg", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.resource.group").Value?.ToString());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_PreservesLocationOverrideInDeploymentState()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        resource.Parameters[AzureBicepResource.KnownParameters.Location] = "westus3";
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(location: AzureLocation.WestUS2);
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal("westus3", section.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
        Assert.False(section.Data.ContainsKey(BicepProvisioner.DeploymentStateProvisioningStateKey));
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_ClearsStaleLocationOverrideWhenEffectiveLocationChanges()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        section.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(section, CancellationToken.None);
 
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(location: AzureLocation.WestUS2);
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.False(section.Data.ContainsKey(AzureProvisioningController.LocationOverrideKey));
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_SavesInProgressDeploymentStateBeforeWaiting()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new WaitingThrowingArmDeploymentCollection());
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<RequestFailedException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage2", section.Data["Id"]?.GetValue<string>());
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateRunning, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
        Assert.True(section.Data.ContainsKey("Parameters"));
        Assert.True(section.Data.ContainsKey("CheckSum"));
        Assert.False(section.Data.ContainsKey("Outputs"));
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_PublishesFailedDeploymentOperationDetailsWhenWaitingFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("search", templateString: "output name string = 'search'");
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new WaitingThrowingArmDeploymentCollection());
        var deploymentId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/search";
        var failedSearchId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/search";
        var failureDetails = new AzureProvisioningFailureDetails(
            Provider: "Microsoft.Search",
            ResourceType: "Microsoft.Search/searchServices",
            ResourceName: "search",
            TargetResourceId: failedSearchId,
            CurrentLocation: null,
            SupportedLocations: [],
            HttpStatus: 400,
            ErrorCode: AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode,
            ErrorMessage: "The provided location 'antarctica' is not available for resource type 'Microsoft.Search/searchServices'.",
            Operation: "deploy",
            RequestId: "request-id",
            CorrelationId: "correlation-id",
            RecommendedActions: AzureProvisioningFailureDetails.GetRecommendedActions(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode));
        var operations = new[]
        {
            new AzureDeploymentOperationDetails(
                OperationId: "search-create",
                DeploymentId: deploymentId,
                ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                ProvisioningState: AzureDeploymentOperationDetails.FailedState,
                Timestamp: DateTimeOffset.Parse("2026-06-11T06:39:22Z"),
                Duration: TimeSpan.FromSeconds(15),
                StatusCode: "BadRequest",
                ServiceRequestId: "service-request-id",
                TargetResource: new AzureDeploymentOperationTarget(failedSearchId, "Microsoft.Search/searchServices", "search"),
                FailureDetails: failureDetails),
            new AzureDeploymentOperationDetails(
                OperationId: "alpha-search-create",
                DeploymentId: deploymentId,
                ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                ProvisioningState: AzureDeploymentOperationDetails.FailedState,
                Timestamp: DateTimeOffset.Parse("2026-06-11T06:39:21Z"),
                Duration: TimeSpan.FromSeconds(14),
                StatusCode: "BadRequest",
                ServiceRequestId: "alpha-service-request-id",
                TargetResource: new AzureDeploymentOperationTarget(
                    "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/alpha-search",
                    "Microsoft.Search/searchServices",
                    "alpha-search"),
                FailureDetails: null),
            new AzureDeploymentOperationDetails(
                OperationId: "nested-deployment",
                DeploymentId: deploymentId,
                ProvisioningOperation: AzureDeploymentOperationDetails.CreateOperation,
                ProvisioningState: AzureDeploymentOperationDetails.SucceededState,
                Timestamp: DateTimeOffset.Parse("2026-06-11T06:39:20Z"),
                Duration: TimeSpan.FromSeconds(5),
                StatusCode: "OK",
                ServiceRequestId: "nested-request-id",
                TargetResource: new AzureDeploymentOperationTarget($"{deploymentId}/nested", AzureDeploymentOperationDetails.DeploymentResourceType, "nested"),
                FailureDetails: null)
        };
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(
                operations,
                new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase)
                {
                    ["Microsoft.Search/searchServices"] = ["eastus", "westus3"]
                }),
            resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<AzureProvisioningFailureException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        var notifications = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("Azure deployment failed", resourceEvent.Snapshot.State?.Text);
        Assert.Collection(
            resourceEvent.Snapshot.Properties,
            property => AssertHighlightedResourceProperty(property, "azure.subscription.id", "12345678-1234-1234-1234-123456789012", AzureProvisioningStrings.ContextPropertySubscriptionIdDisplayName),
            property => AssertHighlightedResourceProperty(property, "azure.resource.group", "test-rg", AzureProvisioningStrings.ContextPropertyResourceGroupDisplayName),
            property => AssertHighlightedResourceProperty(property, "azure.tenant.id", "87654321-4321-4321-4321-210987654321", AzureProvisioningStrings.ContextPropertyTenantIdDisplayName),
            property => AssertHighlightedResourceProperty(property, "azure.tenant.domain", "testdomain.onmicrosoft.com", AzureProvisioningStrings.ContextPropertyTenantDomainDisplayName),
            property => AssertHighlightedResourceProperty(property, "azure.location", "westus2", AzureProvisioningStrings.ContextPropertyLocationDisplayName),
            property => AssertResourceProperty(property, CustomResourceKnownProperties.Source, deploymentId),
            property => AssertHighlightedResourceProperty(property, "azure.deployment.operations.summary", "Failed Azure resources: alpha-search (Microsoft.Search/searchServices), search (Microsoft.Search/searchServices)", AzureProvisioningStrings.DeploymentOperationSummaryDisplayName),
            property => AssertResourceProperty(property, "azure.deployment.operations.total", 3),
            property => AssertResourceProperty(property, "azure.deployment.operations.running", 0),
            property => AssertResourceProperty(property, "azure.deployment.operations.succeeded", 0),
            property => AssertResourceProperty(property, "azure.deployment.operations.failed", 2),
            property => AssertResourceProperty(property, "azure.deployment.operations.canceled", 0),
            property =>
            {
                var failedResources = AssertStringArrayProperty(property, "azure.deployment.operations.failed.resources");
                Assert.Equal(["alpha-search (Microsoft.Search/searchServices)", "search (Microsoft.Search/searchServices)"], failedResources);
            },
            property => AssertResourceProperty(property, "azure.provisioning.error.provider", "Microsoft.Search"),
            property => AssertResourceProperty(property, "azure.provisioning.error.message", "The provided location 'antarctica' is not available for resource type 'Microsoft.Search/searchServices'."),
            property => AssertResourceProperty(property, "azure.provisioning.error.operation", "deploy"),
            property => AssertResourceProperty(property, "azure.provisioning.error.resource.type", "Microsoft.Search/searchServices"),
            property => AssertResourceProperty(property, "azure.provisioning.error.resource.name", "search"),
            property => AssertResourceProperty(property, "azure.provisioning.error.target.resource.id", failedSearchId),
            property => AssertResourceProperty(property, "azure.provisioning.error.current.location", "westus2"),
            property =>
            {
                var supportedLocations = AssertStringArrayProperty(property, "azure.provisioning.error.supported.locations");
                Assert.Equal(["eastus", "westus3"], supportedLocations);
            },
            property => AssertResourceProperty(property, "azure.provisioning.error.code", AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode),
            property => AssertResourceProperty(property, "azure.provisioning.error.http.status", 400),
            property => AssertResourceProperty(property, "azure.provisioning.error.request.id", "request-id"),
            property => AssertResourceProperty(property, "azure.provisioning.error.correlation.id", "correlation-id"),
            property =>
            {
                var recommendedActions = AssertStringArrayProperty(property, "azure.provisioning.error.recommendedActions");
                Assert.Collection(
                    recommendedActions,
                    action => Assert.Contains("change-location --location eastus", action, StringComparison.Ordinal),
                    action => Assert.Contains("Supported regions include: eastus, westus3", action, StringComparison.Ordinal));
            });
 
        Assert.All(
            resourceEvent.Snapshot.Properties.Where(static property =>
                property.Name.StartsWith("azure.deployment.operations.", StringComparison.Ordinal) &&
                !string.Equals(property.Name, "azure.deployment.operations.summary", StringComparison.Ordinal)),
            property => Assert.False(property.IsHighlighted));
 
        static void AssertResourceProperty(ResourcePropertySnapshot property, string name, object value)
        {
            Assert.Equal(name, property.Name);
            Assert.Equal(value, property.Value);
        }
 
        static string[] AssertStringArrayProperty(ResourcePropertySnapshot property, string name)
        {
            Assert.Equal(name, property.Name);
            return Assert.IsType<string[]>(property.Value);
        }
 
        var resourceLogs = await ReadInitialResourceLogsAsync(services.GetRequiredService<ResourceLoggerService>(), resource);
        Assert.Contains(resourceLogs, log =>
            log.IsErrorMessage &&
            log.Content.Contains("Azure provisioning failed:", StringComparison.Ordinal) &&
            log.Content.Contains(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_ClearsStaleDeploymentOperationDetailsWhenDeploymentStartFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("search", templateString: "output name string = 'search'");
        resource.Parameters[AzureBicepResource.KnownParameters.Location] = "australiacentral";
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new LocationUnavailableArmDeploymentCollection());
        var notifications = services.GetRequiredService<ResourceNotificationService>();
 
        await notifications.PublishUpdateAsync(resource, state => state with
        {
            Properties =
            [
                new("azure.deployment.operations.total", 2),
                new("azure.deployment.operations.succeeded", 1),
                new("azure.deployment.operations.succeeded.resources", new[] { "old-search (Microsoft.Search/searchServices)" })
            ]
        });
 
        var provisioner = new BicepProvisioner(
            notifications,
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: new TestArmClient(
                [],
                new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase)
                {
                    ["Microsoft.Search/searchServices"] = ["australiaeast", "westus3"]
                }),
            resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<AzureProvisioningFailureException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.True(notifications.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("Azure deployment failed", resourceEvent.Snapshot.State?.Text);
        Assert.DoesNotContain(resourceEvent.Snapshot.Properties, p => p.Name.StartsWith("azure.deployment.operations.", StringComparison.Ordinal));
        Assert.Equal(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.code").Value);
        Assert.Equal("australiacentral", resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.current.location").Value);
        var supportedLocations = Assert.IsType<string[]>(resourceEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.supported.locations").Value);
        Assert.Equal(["australiaeast", "westus3"], supportedLocations);
 
        var resourceLogs = await ReadInitialResourceLogsAsync(services.GetRequiredService<ResourceLoggerService>(), resource);
        Assert.Contains(resourceLogs, log =>
            log.IsErrorMessage &&
            log.Content.Contains("Azure provisioning failed:", StringComparison.Ordinal) &&
            log.Content.Contains(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_CancelsStartedDeploymentWhenWaitIsCanceled()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var deploymentCollection = new CancelingArmDeploymentCollection();
        var resourceGroup = new DeploymentCollectionResourceGroupResource(deploymentCollection);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<OperationCanceledException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(1, deploymentCollection.CancelCallCount);
        Assert.Equal("storage2", deploymentCollection.CanceledDeploymentName);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_CancelsPendingDeploymentWhenStartIsCanceled()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var deploymentCollection = new PendingCancelArmDeploymentCollection();
        var resourceGroup = new DeploymentCollectionResourceGroupResource(deploymentCollection);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<OperationCanceledException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(1, deploymentCollection.CancelCallCount);
        Assert.Equal("storage2", deploymentCollection.CanceledDeploymentName);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_PersistsCanceledStateWhenCancelFindsAlreadyInactiveDeployment()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var deploymentCollection = new AlreadyCanceledArmDeploymentCollection();
        var resourceGroup = new DeploymentCollectionResourceGroupResource(deploymentCollection);
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        await Assert.ThrowsAsync<OperationCanceledException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(1, deploymentCollection.CancelCallCount);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_AdoptsActiveCachedDeploymentWhenCreateReportsDeploymentActive()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(AzureDeploymentOperationDetails.RunningState, Outputs: null));
        armClient.DeploymentStates.Enqueue(new(
            AzureDeploymentOperationDetails.SucceededState,
            CreateDeploymentOutputs("https://storage.blob.core.windows.net/")));
        var deploymentCollection = new ActiveDeploymentConflictArmDeploymentCollection();
        var resourceGroup = new DeploymentCollectionResourceGroupResource(deploymentCollection);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            resourceGroup: resourceGroup,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run, new ImmediateTimeProvider());
 
        await provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None);
 
        Assert.Equal(1, deploymentCollection.CreateCallCount);
        Assert.Equal(resource.Name, deploymentCollection.DeploymentName);
        Assert.Equal(2, armClient.GetDeploymentCallCount);
        Assert.Equal("https://storage.blob.core.windows.net/", resource.Outputs["blobEndpoint"]);
 
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Null(section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]);
        Assert.Equal(deploymentId, section.Data[BicepUtilities.DeploymentStateIdKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_DoesNotAdoptActiveDeploymentWhenCachedChecksumDoesNotMatch()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        resource.Parameters["sku"] = "Standard_LRS";
        var armClient = new TestArmClient();
        var deploymentCollection = new ActiveDeploymentConflictArmDeploymentCollection();
        var resourceGroup = new DeploymentCollectionResourceGroupResource(deploymentCollection);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            resourceGroup: resourceGroup,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run, new ImmediateTimeProvider());
 
        await Assert.ThrowsAsync<RequestFailedException>(() =>
            provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Equal(1, deploymentCollection.CreateCallCount);
        Assert.Equal(0, armClient.GetDeploymentCallCount);
    }
 
    [Fact]
    public async Task GetOrCreateResourceAsync_SavesTerminalDeploymentStateWhenDeploymentFails()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(ProvisioningTestHelpers.CreateUserSecretsManager());
        using var services = builder.Services.BuildServiceProvider();
 
        var deploymentStateManager = services.GetRequiredService<IDeploymentStateManager>();
        var resource = new AzureBicepResource("storage2", templateString: "output name string = 'storage2'");
        var resourceGroup = new DeploymentCollectionResourceGroupResource(new FailingArmDeploymentCollection());
 
        var provisioner = new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            deploymentStateManager,
            new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance);
 
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(resourceGroup: resourceGroup);
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => provisioner.GetOrCreateResourceAsync(resource, context, CancellationToken.None));
 
        Assert.Contains(ResourcesProvisioningState.Failed.ToString(), exception.Message);
 
        var section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2", CancellationToken.None);
        Assert.Equal(ResourcesProvisioningState.Failed.ToString(), section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task BicepCliExecutor_CompilesBicepToArm()
    {
        // Test the mock bicep executor behavior
 
        // Arrange
        var bicepExecutor = new TestBicepCliExecutor();
 
        // Act
        var result = await bicepExecutor.CompileBicepToArmAsync("test.bicep", CancellationToken.None);
 
        // Assert
        Assert.True(bicepExecutor.CompileBicepToArmAsyncCalled);
        Assert.Equal("test.bicep", bicepExecutor.LastCompiledPath);
        Assert.NotNull(result);
        Assert.Contains("$schema", result);
    }
 
    [Fact]
    public void SecretClientProvider_CreatesSecretClient()
    {
        // Test the mock secret client provider behavior
 
        // Arrange
        var secretClientProvider = new TestSecretClientProvider();
        var vaultUri = new Uri("https://test.vault.azure.net/");
 
        // Act
        var client = secretClientProvider.GetSecretClient(vaultUri);
 
        // Assert
        Assert.True(secretClientProvider.GetSecretClientCalled);
        // Client will be null in our mock, but the call was tracked
        Assert.Null(client);
    }
 
    [Fact]
    public void TestTokenCredential_ProvidesAccessToken()
    {
        // Test the mock token credential behavior
 
        // Arrange
        var tokenProvider = new TestTokenCredentialProvider();
        var credential = tokenProvider.TokenCredential;
        var requestContext = new TokenRequestContext(["https://management.azure.com/.default"]);
 
        // Act
        var token = credential.GetToken(requestContext, CancellationToken.None);
 
        // Assert
        Assert.Equal("mock-token", token.Token);
        Assert.True(token.ExpiresOn > DateTimeOffset.UtcNow);
    }
 
    [Fact]
    public async Task TestTokenCredential_ProvidesAccessTokenAsync()
    {
        // Test the mock token credential async behavior
 
        // Arrange
        var provider = new TestTokenCredentialProvider();
        var credential = provider.TokenCredential;
        var requestContext = new TokenRequestContext(["https://management.azure.com/.default"]);
 
        // Act
        var token = await credential.GetTokenAsync(requestContext, CancellationToken.None);
 
        // Assert
        Assert.Equal("mock-token", token.Token);
        Assert.True(token.ExpiresOn > DateTimeOffset.UtcNow);
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_ConfiguresSucceededDeploymentFromArm()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(
            AzureDeploymentOperationDetails.SucceededState,
            CreateDeploymentOutputs("https://storage.blob.core.windows.net/")));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.True(result);
        Assert.Equal("https://storage.blob.core.windows.net/", resource.Outputs["blobEndpoint"]);
        Assert.Equal(1, armClient.GetDeploymentCallCount);
 
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Null(section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]);
        Assert.Equal(deploymentId, section.Data[BicepUtilities.DeploymentStateIdKey]?.GetValue<string>());
        var outputs = JsonNode.Parse(section.Data[BicepUtilities.DeploymentStateOutputsKey]!.GetValue<string>())!.AsObject();
        Assert.Equal("https://storage.blob.core.windows.net/", outputs["blobEndpoint"]?["value"]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_WaitsForRunningDeploymentBeforeConfiguring()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(AzureDeploymentOperationDetails.RunningState, Outputs: null));
        armClient.DeploymentStates.Enqueue(new(
            AzureDeploymentOperationDetails.SucceededState,
            CreateDeploymentOutputs("https://storage.blob.core.windows.net/")));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run, new ImmediateTimeProvider());
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.True(result);
        Assert.Equal("https://storage.blob.core.windows.net/", resource.Outputs["blobEndpoint"]);
        Assert.Equal(2, armClient.GetDeploymentCallCount);
        Assert.True(armClient.DeploymentOperationsCallCount > 0);
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_ClearsStaleRunningStateWhenDeploymentIsMissing()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(null);
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.False(result);
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Empty(section.Data);
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_LeavesRunningStateWhenArmCannotBeQueried()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient
        {
            GetDeploymentException = new global::Azure.Identity.CredentialUnavailableException("Credential unavailable.")
        };
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.False(result);
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateRunning, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
        Assert.Equal(deploymentId, section.Data[BicepUtilities.DeploymentStateIdKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_LeavesRunningStateWhenArmFailsDuringWait()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentProbeResults.Enqueue(new AzureDeploymentState(AzureDeploymentOperationDetails.RunningState, Outputs: null));
        armClient.DeploymentProbeResults.Enqueue(new RequestFailedException(503, "Service unavailable."));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run, new ImmediateTimeProvider());
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.False(result);
        Assert.Equal(2, armClient.GetDeploymentCallCount);
        Assert.False(resource.Outputs.ContainsKey("blobEndpoint"));
 
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateRunning, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
        Assert.Equal(deploymentId, section.Data[BicepUtilities.DeploymentStateIdKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_PersistsFailedStateAndThrowsWhenDeploymentFailed()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(AzureDeploymentOperationDetails.FailedState, Outputs: null));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None));
 
        Assert.Contains(resource.Name, exception.Message, StringComparison.Ordinal);
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateFailed, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_PersistsCanceledStateAndThrowsWhenDeploymentCanceled()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(AzureDeploymentOperationDetails.CanceledState, Outputs: null));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None));
 
        Assert.Contains(resource.Name, exception.Message, StringComparison.Ordinal);
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
 
        var notificationService = services.GetRequiredService<ResourceNotificationService>();
        Assert.True(notificationService.TryGetCurrentState(resource.Name, out var resourceEvent));
        Assert.Equal("Azure deployment canceled", resourceEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task ReconcileDeploymentStateAsync_ReturnsFalseWhenSucceededDeploymentChecksumDoesNotMatch()
    {
        var deploymentStateManager = new InMemoryDeploymentStateManager();
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        using var services = builder.Services.BuildServiceProvider();
        var resource = CreateReconciledStorageResource();
        var deploymentId = GetTestDeploymentId(resource.Name);
        await SeedRunningDeploymentStateAsync(deploymentStateManager, resource, deploymentId);
        resource.Parameters["sku"] = "Standard_LRS";
        var armClient = new TestArmClient();
        armClient.DeploymentStates.Enqueue(new(
            AzureDeploymentOperationDetails.SucceededState,
            CreateDeploymentOutputs("https://storage.blob.core.windows.net/")));
        var context = ProvisioningTestHelpers.CreateTestProvisioningContext(
            armClient: armClient,
            executionContext: new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run));
        var provisioner = CreateProvisioner(services, DistributedApplicationOperation.Run);
 
        var result = await provisioner.ReconcileDeploymentStateAsync(resource, context, CancellationToken.None);
 
        Assert.False(result);
        Assert.False(resource.Outputs.ContainsKey("blobEndpoint"));
 
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        Assert.Null(section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]);
        Assert.Equal(deploymentId, section.Data[BicepUtilities.DeploymentStateIdKey]?.GetValue<string>());
    }
 
    private static void AssertResourceGroupPropertyIsNull(IServiceProvider services, string resourceName)
    {
        var notificationService = services.GetRequiredService<ResourceNotificationService>();
        var resourceEvent = notificationService.TryGetCurrentState(resourceName, out var currentState)
            ? currentState
            : throw new InvalidOperationException($"Expected current state for resource '{resourceName}'.");
        var property = Assert.Single(resourceEvent.Snapshot.Properties, p => p.Name == "azure.resource.group");
 
        Assert.Null(property.Value);
    }
 
    private static void AssertHighlightedContextProperty(IEnumerable<ResourcePropertySnapshot> properties, string name, object value, string displayName)
    {
        var property = Assert.Single(properties, p => p.Name == name);
        AssertHighlightedResourceProperty(property, name, value, displayName);
    }
 
    private static void AssertHighlightedResourceProperty(ResourcePropertySnapshot property, string name, object value, string displayName)
    {
        Assert.Equal(name, property.Name);
        Assert.Equal(value, property.Value);
        Assert.Equal(displayName, property.DisplayName);
        Assert.True(property.IsHighlighted);
    }
 
    private static async Task<IReadOnlyList<(string Content, bool IsErrorMessage)>> ReadInitialResourceLogsAsync(ResourceLoggerService loggerService, IResource resource)
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        await using var logEnumerator = loggerService.WatchAsync(resource).GetAsyncEnumerator(cts.Token);
 
        if (!await logEnumerator.MoveNextAsync())
        {
            return [];
        }
 
        return [.. logEnumerator.Current.Select(static log => (log.Content, log.IsErrorMessage))];
    }
 
    private static AzureBicepResource CreateReconciledStorageResource()
    {
        var resource = new AzureBicepResource("storage", templateString: "output blobEndpoint string = 'https://storage.blob.core.windows.net/'");
        resource.Parameters[AzureBicepResource.KnownParameters.Location] = "westus2";
        return resource;
    }
 
    private static async Task SeedRunningDeploymentStateAsync(IDeploymentStateManager deploymentStateManager, AzureBicepResource resource, string deploymentId)
    {
        var parameters = new JsonObject();
        await BicepUtilities.SetParametersAsync(parameters, resource);
 
        var scope = new JsonObject();
        await BicepUtilities.SetScopeAsync(scope, resource);
 
        var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}");
        section.Data[BicepUtilities.DeploymentStateIdKey] = deploymentId;
        section.Data[BicepUtilities.DeploymentStateParametersKey] = parameters.ToJsonString();
        section.Data[BicepUtilities.DeploymentStateScopeKey] = scope.ToJsonString();
        section.Data[BicepUtilities.DeploymentStateChecksumKey] = BicepUtilities.GetChecksum(resource, parameters, scope);
        section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(section);
    }
 
    private static JsonObject CreateDeploymentOutputs(string blobEndpoint)
        => new()
        {
            ["blobEndpoint"] = new JsonObject
            {
                ["value"] = blobEndpoint
            }
        };
 
    private static string GetTestDeploymentId(string deploymentName)
        => $"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/{deploymentName}";
 
    private static BicepProvisioner CreateProvisioner(IServiceProvider services, DistributedApplicationOperation operation = DistributedApplicationOperation.Publish, TimeProvider? timeProvider = null)
    {
        return new BicepProvisioner(
            services.GetRequiredService<ResourceNotificationService>(),
            services.GetRequiredService<ResourceLoggerService>(),
            new TestBicepCliExecutor(),
            new TestSecretClientProvider(),
            services.GetRequiredService<IDeploymentStateManager>(),
            new DistributedApplicationExecutionContext(operation),
            services.GetRequiredService<IFileSystemService>(),
            NullLogger<BicepProvisioner>.Instance,
            timeProvider);
    }
 
    private sealed class TestTokenCredentialProvider : ITokenCredentialProvider
    {
        public TokenCredential TokenCredential => new MockTokenCredential();
 
        private sealed class MockTokenCredential : TokenCredential
        {
            public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
                new("mock-token", DateTimeOffset.UtcNow.AddHours(1));
 
            public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
                ValueTask.FromResult(new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1)));
        }
    }
 
    private sealed class TestBicepCliExecutor : IBicepCompiler
    {
        public bool CompileBicepToArmAsyncCalled { get; private set; }
        public string? LastCompiledPath { get; private set; }
        public string CompilationResult { get; set; } = """{"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"}""";
 
        public Task<string> CompileBicepToArmAsync(string bicepFilePath, CancellationToken cancellationToken = default)
        {
            CompileBicepToArmAsyncCalled = true;
            LastCompiledPath = bicepFilePath;
            return Task.FromResult(CompilationResult);
        }
    }
 
    private sealed class ImmediateTimeProvider : TimeProvider
    {
        public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
        {
            var timer = new ImmediateTimer();
            callback(state);
            return timer;
        }
 
        private sealed class ImmediateTimer : ITimer
        {
            public bool Change(TimeSpan dueTime, TimeSpan period) => true;
 
            public void Dispose()
            {
            }
 
            public ValueTask DisposeAsync() => ValueTask.CompletedTask;
        }
    }
 
    private sealed class TestSecretClientProvider : ISecretClientProvider
    {
        public bool GetSecretClientCalled { get; private set; }
 
        public SecretClient GetSecretClient(Uri vaultUri)
        {
            GetSecretClientCalled = true;
            // Return null - this will fail in actual secret operations but allows testing the call
            return null!;
        }
    }
 
    private sealed class MockDeploymentStateManager : IDeploymentStateManager
    {
        public string? StateFilePath => null;
 
        public Task<DeploymentStateSection> AcquireSectionAsync(string sectionName, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new DeploymentStateSection(sectionName, [], 0));
        }
 
        public Task<DeploymentStateSection> AcquireCurrentSectionAsync(string sectionName, CancellationToken cancellationToken = default)
            => AcquireSectionAsync(sectionName, cancellationToken);
 
        public Task DeleteSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }
 
        public Task SaveSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }
 
        public Task ClearAllStateAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private sealed class DeploymentCollectionResourceGroupResource(IArmDeploymentCollection armDeploymentCollection) : IResourceGroupResource
    {
        public ResourceIdentifier Id { get; } = new("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg");
        public string Name => "test-rg";
 
        public IArmDeploymentCollection GetArmDeployments() => armDeploymentCollection;
 
        public Task<ArmOperation> DeleteAsync(WaitUntil waitUntil, CancellationToken cancellationToken = default) =>
            Task.FromResult<ArmOperation>(new TestDeleteArmOperation());
 
        public async IAsyncEnumerable<(string Name, string ResourceType)> GetResourcesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class ThrowingResourceGroupResource(string name) : IResourceGroupResource
    {
        private int _deleteCallCount;
 
        public int DeleteCallCount => _deleteCallCount;
 
        public ResourceIdentifier Id => new($"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/{name}");
        public string Name => name;
 
        public IArmDeploymentCollection GetArmDeployments() => new ThrowingArmDeploymentCollection();
 
        public Task<ArmOperation> DeleteAsync(WaitUntil waitUntil, CancellationToken cancellationToken = default)
        {
            _deleteCallCount++;
            return Task.FromResult<ArmOperation>(new TestDeleteArmOperation());
        }
 
        public async IAsyncEnumerable<(string Name, string ResourceType)> GetResourcesAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class WaitingThrowingSubscriptionResource : ISubscriptionResource
    {
        public ResourceIdentifier Id { get; } = new("/subscriptions/12345678-1234-1234-1234-123456789012");
 
        public string? DisplayName => "Test Subscription";
 
        public Guid? TenantId => Guid.Parse("87654321-4321-4321-4321-210987654321");
 
        public IResourceGroupCollection GetResourceGroups() => new TestResourceGroupCollection();
 
        public IArmDeploymentCollection GetArmDeployments() => new WaitingThrowingArmDeploymentCollection();
    }
 
    private sealed class WaitingThrowingArmDeploymentCollection : IArmDeploymentCollection
    {
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<ArmOperation<ArmDeploymentResource>>(new WaitingThrowingArmDeploymentOperation());
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private sealed class WaitingThrowingArmDeploymentOperation : ArmOperation<ArmDeploymentResource>
    {
        public override string Id { get; } = Guid.NewGuid().ToString();
 
        public override ArmDeploymentResource Value => throw new InvalidOperationException("The deployment did not complete successfully.");
 
        public override bool HasCompleted => false;
 
        public override bool HasValue => false;
 
        public override Response GetRawResponse() => new MockResponse(200);
 
        public override Response UpdateStatus(CancellationToken cancellationToken = default) => new MockResponse(200);
 
        public override ValueTask<Response> UpdateStatusAsync(CancellationToken cancellationToken = default) => ValueTask.FromResult<Response>(new MockResponse(200));
 
        public override ValueTask<Response<ArmDeploymentResource>> WaitForCompletionAsync(CancellationToken cancellationToken = default) =>
            ValueTask.FromException<Response<ArmDeploymentResource>>(new RequestFailedException(409, "Deployment wait failed."));
 
        public override ValueTask<Response<ArmDeploymentResource>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) =>
            ValueTask.FromException<Response<ArmDeploymentResource>>(new RequestFailedException(409, "Deployment wait failed."));
 
        public override Response<ArmDeploymentResource> WaitForCompletion(CancellationToken cancellationToken = default) =>
            throw new RequestFailedException(409, "Deployment wait failed.");
 
        public override Response<ArmDeploymentResource> WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken = default) =>
            throw new RequestFailedException(409, "Deployment wait failed.");
    }
 
    private sealed class ThrowingArmDeploymentCollection : IArmDeploymentCollection
    {
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            throw new RequestFailedException(409, "Deployment creation failed.");
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private sealed class ActiveDeploymentConflictArmDeploymentCollection : IArmDeploymentCollection
    {
        public int CreateCallCount { get; private set; }
        public string? DeploymentName { get; private set; }
 
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default)
        {
            CreateCallCount++;
            DeploymentName = deploymentName;
            return Task.FromException<ArmOperation<ArmDeploymentResource>>(new RequestFailedException(
                409,
                "The deployment is still active.",
                "DeploymentActive",
                innerException: null));
        }
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private sealed class LocationUnavailableArmDeploymentCollection : IArmDeploymentCollection
    {
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromException<ArmOperation<ArmDeploymentResource>>(new RequestFailedException(
                400,
                "The provided location 'australiacentral' is not available for resource type 'Microsoft.Search/searchServices'.",
                AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode,
                innerException: null));
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
 
    private sealed class CancelingArmDeploymentCollection : IArmDeploymentCollection
    {
        public int CancelCallCount { get; private set; }
        public string? CanceledDeploymentName { get; private set; }
 
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<ArmOperation<ArmDeploymentResource>>(new CancelingArmDeploymentOperation());
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default)
        {
            CancelCallCount++;
            CanceledDeploymentName = deploymentName;
            return Task.CompletedTask;
        }
    }
 
    private sealed class CancelingArmDeploymentOperation : ArmOperation<ArmDeploymentResource>
    {
        public override string Id { get; } = Guid.NewGuid().ToString();
 
        public override ArmDeploymentResource Value => throw new InvalidOperationException("The deployment did not complete successfully.");
 
        public override bool HasCompleted => false;
 
        public override bool HasValue => false;
 
        public override Response GetRawResponse() => new MockResponse(200);
 
        public override Response UpdateStatus(CancellationToken cancellationToken = default) => new MockResponse(200);
 
        public override ValueTask<Response> UpdateStatusAsync(CancellationToken cancellationToken = default) => ValueTask.FromResult<Response>(new MockResponse(200));
 
        public override ValueTask<Response<ArmDeploymentResource>> WaitForCompletionAsync(CancellationToken cancellationToken = default) =>
            ValueTask.FromException<Response<ArmDeploymentResource>>(new OperationCanceledException(cancellationToken));
 
        public override ValueTask<Response<ArmDeploymentResource>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken = default) =>
            ValueTask.FromException<Response<ArmDeploymentResource>>(new OperationCanceledException(cancellationToken));
 
        public override Response<ArmDeploymentResource> WaitForCompletion(CancellationToken cancellationToken = default) =>
            throw new OperationCanceledException(cancellationToken);
 
        public override Response<ArmDeploymentResource> WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken = default) =>
            throw new OperationCanceledException(cancellationToken);
    }
 
    private sealed class PendingCancelArmDeploymentCollection : IArmDeploymentCollection
    {
        public int CancelCallCount { get; private set; }
        public string? CanceledDeploymentName { get; private set; }
 
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromException<ArmOperation<ArmDeploymentResource>>(new OperationCanceledException(cancellationToken));
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default)
        {
            CancelCallCount++;
            CanceledDeploymentName = deploymentName;
            return Task.CompletedTask;
        }
    }
 
    private sealed class AlreadyCanceledArmDeploymentCollection : IArmDeploymentCollection
    {
        public int CancelCallCount { get; private set; }
 
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<ArmOperation<ArmDeploymentResource>>(new CancelingArmDeploymentOperation());
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default)
        {
            CancelCallCount++;
            throw new RequestFailedException(409, "The deployment is already canceled.");
        }
    }
 
    private sealed class FailingArmDeploymentCollection : IArmDeploymentCollection
    {
        public Task<ArmOperation<ArmDeploymentResource>> CreateOrUpdateAsync(
            WaitUntil waitUntil,
            string deploymentName,
            ArmDeploymentContent content,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<ArmOperation<ArmDeploymentResource>>(new TestArmOperation<ArmDeploymentResource>(
                new TestArmDeploymentResource(deploymentName, [], ResourcesProvisioningState.Failed)));
 
        public Task CancelAsync(string deploymentName, CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
}