File: AzureEnvironmentResourceExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.Tests)
#pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREPIPELINES002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Utils;
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 Azure;
using Azure.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureEnvironmentResourceExtensionsTests
{
    [Fact]
    public void AddAzureEnvironment_ShouldAddResourceToBuilder_InPublishMode()
    {
        // Arrange
        var builder = CreateBuilder();
 
        // Act
        var resourceBuilder = builder.AddAzureEnvironment();
 
        // Assert
        Assert.NotNull(resourceBuilder);
        var environmentResource = Assert.Single(builder.Resources.OfType<AzureEnvironmentResource>());
        // Assert that default Location and ResourceGroup parameters are set
        Assert.NotNull(environmentResource.Location);
        Assert.NotNull(environmentResource.ResourceGroupName);
        // Assert that the parameters are not added to the resource model
        Assert.Empty(builder.Resources.OfType<ParameterResource>());
    }
 
    [Fact]
    public void AddAzureEnvironment_CalledMultipleTimes_ReturnsSameResource()
    {
        // Arrange
        var builder = CreateBuilder();
 
        // Act
        var firstBuilder = builder.AddAzureEnvironment();
        var secondBuilder = builder.AddAzureEnvironment();
 
        // Assert
        Assert.Same(firstBuilder.Resource, secondBuilder.Resource);
        Assert.Single(builder.Resources.OfType<AzureEnvironmentResource>());
    }
 
    [Fact]
    public void AddAzureEnvironment_InRunMode_AddsControlResourceWithResetCommand()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        var resourceBuilder = builder.AddAzureEnvironment();
 
        Assert.NotNull(resourceBuilder);
        var environmentResource = Assert.Single(builder.Resources.OfType<AzureEnvironmentResource>());
        var resetCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ResetProvisioningStateCommandName);
        Assert.Equal("Reset provisioning state", resetCommand.DisplayName);
        Assert.Contains("not delete live Azure resources", resetCommand.DisplayDescription);
        Assert.Contains("may be left orphaned", resetCommand.ConfirmationMessage);
    }
 
    [Fact]
    public void AddAzureEnvironment_InRunMode_AddsCommandsInDefinitionOrder()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        builder.AddAzureEnvironment();
 
        var environmentResource = Assert.Single(builder.Resources.OfType<AzureEnvironmentResource>());
        var commands = environmentResource.Annotations.OfType<ResourceCommandAnnotation>().ToArray();
 
        Assert.Collection(commands,
            command =>
            {
                Assert.Equal(AzureProvisioningController.ResetProvisioningStateCommandName, command.Name);
                Assert.True(command.IsHighlighted);
            },
            command =>
            {
                Assert.Equal(AzureProvisioningController.ChangeAzureContextCommandName, command.Name);
                Assert.True(command.IsHighlighted);
            },
            command =>
            {
                Assert.Equal(AzureProvisioningController.ReprovisionAllCommandName, command.Name);
                Assert.False(command.IsHighlighted);
            },
            command =>
            {
                Assert.Equal(AzureProvisioningController.DeleteAzureResourcesCommandName, command.Name);
                Assert.False(command.IsHighlighted);
            });
    }
 
    [Fact]
    public void AddAzureEnvironment_InRunMode_AddsSelectableArgumentsToChangeAzureContextCommand()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        builder.AddAzureEnvironment();
 
        var environmentResource = Assert.Single(builder.Resources.OfType<AzureEnvironmentResource>());
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
 
        Assert.NotNull(changeContextCommand.ValidateArguments);
        Assert.Collection(changeContextCommand.Arguments,
            input =>
            {
                Assert.Equal("tenantId", input.Name);
                Assert.True(input.Required);
                Assert.Equal(InputType.Choice, input.InputType);
                Assert.True(input.AllowCustomChoice);
                Assert.True(input.Disabled);
                Assert.NotNull(input.DynamicLoading);
                Assert.True(input.DynamicLoading.AlwaysLoadOnStart);
            },
            input =>
            {
                Assert.Equal("subscriptionId", input.Name);
                Assert.True(input.Required);
                Assert.Equal(InputType.Choice, input.InputType);
                Assert.True(input.AllowCustomChoice);
                Assert.True(input.Disabled);
                Assert.NotNull(input.DynamicLoading);
                Assert.True(input.DynamicLoading.AlwaysLoadOnStart);
                Assert.Equal(["tenantId"], input.DynamicLoading.DependsOnInputs);
            },
            input =>
            {
                Assert.Equal("resourceGroup", input.Name);
                Assert.True(input.Required);
                Assert.Equal(InputType.Choice, input.InputType);
                Assert.True(input.AllowCustomChoice);
                Assert.True(input.Disabled);
                Assert.NotNull(input.DynamicLoading);
                Assert.True(input.DynamicLoading.AlwaysLoadOnStart);
                Assert.Equal(["subscriptionId"], input.DynamicLoading.DependsOnInputs);
            },
            input =>
            {
                Assert.Equal(AzureBicepResource.KnownParameters.Location, input.Name);
                Assert.True(input.Required);
                Assert.Equal(InputType.Choice, input.InputType);
                Assert.True(input.AllowCustomChoice);
                Assert.True(input.Disabled);
                Assert.NotNull(input.DynamicLoading);
                Assert.True(input.DynamicLoading.AlwaysLoadOnStart);
                Assert.Equal(["subscriptionId", "resourceGroup"], input.DynamicLoading.DependsOnInputs);
            });
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_DynamicArgumentsLoadAzureContextOptions()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        builder.Configuration["Azure:TenantId"] = "87654321-4321-4321-4321-210987654321";
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:ResourceGroup"] = "rg-test-2";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder);
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
        var inputs = CloneInputs(changeContextCommand.Arguments);
 
        var tenantInput = inputs["tenantId"];
        Assert.True(tenantInput.Disabled);
 
        await LoadInputAsync(app.Services, inputs, tenantInput);
 
        Assert.False(tenantInput.Disabled);
        Assert.Contains(tenantInput.Options!, option => option.Key == "87654321-4321-4321-4321-210987654321");
        Assert.Equal("87654321-4321-4321-4321-210987654321", tenantInput.Value);
 
        var subscriptionInput = inputs["subscriptionId"];
        Assert.True(subscriptionInput.Disabled);
        Assert.True(subscriptionInput.DynamicLoading!.AlwaysLoadOnStart);
 
        await LoadInputAsync(app.Services, inputs, subscriptionInput);
 
        Assert.False(subscriptionInput.Disabled);
        Assert.Contains(subscriptionInput.Options!, option => option.Key == "12345678-1234-1234-1234-123456789012");
        Assert.Equal("12345678-1234-1234-1234-123456789012", subscriptionInput.Value);
 
        var resourceGroupInput = inputs["resourceGroup"];
        Assert.True(resourceGroupInput.Disabled);
 
        await LoadInputAsync(app.Services, inputs, resourceGroupInput);
 
        Assert.False(resourceGroupInput.Disabled);
        Assert.Contains(resourceGroupInput.Options!, option => option.Key == "rg-test-2");
        Assert.Equal("rg-test-2", resourceGroupInput.Value);
 
        var locationInput = inputs[AzureBicepResource.KnownParameters.Location];
        Assert.True(locationInput.Disabled);
 
        await LoadInputAsync(app.Services, inputs, locationInput);
 
        Assert.True(locationInput.Disabled);
        Assert.Equal("westus", locationInput.Value);
        Assert.Equal([KeyValuePair.Create("westus", "westus")], locationInput.Options);
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_TenantChangeReloadsSubscriptionOptionsForSelectedTenant()
    {
        var builder = CreateBuilder(isRunMode: true);
        var armClient = new AzureContextOptionsArmClient();
 
        builder.Configuration["Azure:TenantId"] = AzureContextOptionsArmClient.FirstTenantId;
        builder.Configuration["Azure:SubscriptionId"] = AzureContextOptionsArmClient.FirstSubscriptionId;
        builder.Configuration["Azure:ResourceGroup"] = "rg-first";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder, armClientProvider: new AzureContextOptionsArmClientProvider(armClient));
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
        var inputs = CloneInputs(changeContextCommand.Arguments);
 
        var tenantInput = inputs["tenantId"];
        await LoadInputAsync(app.Services, inputs, tenantInput);
 
        Assert.Contains(tenantInput.Options!, option => option.Key == AzureContextOptionsArmClient.SecondTenantId);
        Assert.Equal(AzureContextOptionsArmClient.FirstTenantId, tenantInput.Value);
 
        tenantInput.Value = AzureContextOptionsArmClient.SecondTenantId;
        var subscriptionInput = inputs["subscriptionId"];
        subscriptionInput.Disabled = true;
 
        await LoadInputAsync(app.Services, inputs, subscriptionInput);
 
        Assert.False(subscriptionInput.Disabled);
        Assert.Equal(AzureContextOptionsArmClient.SecondTenantId, armClient.LastSubscriptionTenantId);
        Assert.DoesNotContain(subscriptionInput.Options!, option => option.Key == AzureContextOptionsArmClient.FirstSubscriptionId);
        Assert.Contains(subscriptionInput.Options!, option => option.Key == AzureContextOptionsArmClient.SecondSubscriptionId);
 
        subscriptionInput.Value = AzureContextOptionsArmClient.SecondSubscriptionId;
        var resourceGroupInput = inputs["resourceGroup"];
 
        await LoadInputAsync(app.Services, inputs, resourceGroupInput);
 
        Assert.Equal(AzureContextOptionsArmClient.SecondSubscriptionId, armClient.LastResourceGroupSubscriptionId);
        Assert.Contains(resourceGroupInput.Options!, option => option.Key == "rg-second");
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_CustomResourceGroupEnablesLocationChoices()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        builder.Configuration["Azure:TenantId"] = "87654321-4321-4321-4321-210987654321";
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:ResourceGroup"] = "rg-new";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder);
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
        var inputs = CloneInputs(changeContextCommand.Arguments);
        var subscriptionInput = inputs["subscriptionId"];
        subscriptionInput.Value = "12345678-1234-1234-1234-123456789012";
        var resourceGroupInput = inputs["resourceGroup"];
        resourceGroupInput.Value = "rg-new";
        var locationInput = inputs[AzureBicepResource.KnownParameters.Location];
        locationInput.Disabled = true;
 
        await LoadInputAsync(app.Services, inputs, locationInput);
 
        Assert.False(locationInput.Disabled);
        Assert.Contains(locationInput.Options!, option => option.Key == "eastus");
        Assert.Contains(locationInput.Options!, option => option.Key == "westus2");
        Assert.Equal("eastus", locationInput.Value);
    }
 
    [Fact]
    public async Task ResetProvisioningStateCommand_ClearsCachedStateAndResetsSnapshots()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "sub";
        azureSection.Data["Location"] = "westus2";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Resources/deployments/storage";
        storageSection.Data["CheckSum"] = "checksum";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        storage.Resource.Outputs["blobEndpoint"] = "https://storage.blob.core.windows.net/";
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties =
            [
                new("azure.subscription.id", "sub")
            ]
        });
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Urls =
            [
                new("deployment", "https://portal.azure.com", false)
            ],
            Properties =
            [
                new("azure.subscription.id", "sub"),
                new(CustomResourceKnownProperties.Source, "deployment-id"),
                new("custom.property", "keep")
            ]
        });
 
        var resetCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ResetProvisioningStateCommandName);
 
        var result = await resetCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure provisioning state reset.", result.Message);
        var resultData = AssertCommandJsonData(result);
        Assert.Contains("orphaned", resultData["warning"]?.GetValue<string>(), StringComparison.Ordinal);
        var recommendedActions = Assert.IsType<JsonArray>(resultData["recommendedActions"]);
        Assert.Contains(recommendedActions, action => action?["code"]?.GetValue<string>() == "delete-live-resources");
 
        azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.Empty(azureSection.Data);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
 
        Assert.Empty(storage.Resource.Outputs);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(KnownResourceStates.NotStarted, environmentEvent.Snapshot.State?.Text);
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Enabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.NotStarted, storageEvent.Snapshot.State?.Text);
        Assert.Empty(storageEvent.Snapshot.Urls);
        Assert.DoesNotContain(storageEvent.Snapshot.Properties, p => p.Name == "azure.subscription.id");
        Assert.DoesNotContain(storageEvent.Snapshot.Properties, p => p.Name == CustomResourceKnownProperties.Source);
        Assert.Contains(storageEvent.Snapshot.Properties, p => p.Name == "custom.property");
    }
 
    [Fact]
    public async Task ResetProvisioningStateCommand_ReentersProvisioningAndPromptsWhenAzureConfigMissing()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
 
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var resetCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ResetProvisioningStateCommandName);
 
        var commandTask = resetCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var result = await commandTask.WaitAsync(s_testSynchronizationTimeout);
 
        Assert.False(result.Success);
        Assert.True(result.Canceled);
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().WaitAsync(s_testSynchronizationTimeout);
 
        Assert.Equal(AzureProvisioningStrings.NotificationTitle, interaction.Title);
        Assert.Equal(AzureProvisioningStrings.NotificationMessage, interaction.Message);
        var options = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Warning, options.Intent);
        Assert.Equal(AzureProvisioningStrings.NotificationPrimaryButtonText, options.PrimaryButtonText);
    }
 
    [Fact]
    public async Task MissingAzureContextNotification_ReappearsWhenConfigureDialogIsCanceled()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
 
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.EnsureProvisionedAsync(model);
 
        var notification = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().WaitAsync(s_testSynchronizationTimeout);
 
        Assert.Equal(AzureProvisioningStrings.NotificationTitle, notification.Title);
        Assert.Equal(AzureProvisioningStrings.NotificationMessage, notification.Message);
 
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(notification.Options);
        Assert.Equal(MessageIntent.Warning, notificationOptions.Intent);
        Assert.Equal(AzureProvisioningStrings.NotificationPrimaryButtonText, notificationOptions.PrimaryButtonText);
 
        notification.CompletionTcs.SetResult(InteractionResult.Ok(true));
 
        var inputs = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().WaitAsync(s_testSynchronizationTimeout);
 
        Assert.Equal(AzureProvisioningStrings.InputsTitle, inputs.Title);
        Assert.Equal(AzureProvisioningStrings.InputsMessage, inputs.Message);
        var inputOptions = Assert.IsType<InputsDialogInteractionOptions>(inputs.Options);
        Assert.True(inputOptions.EnableMessageMarkdown);
        Assert.Equal(AzureProvisioningStrings.InputsPrimaryButtonText, inputOptions.PrimaryButtonText);
        Assert.Equal(AzureProvisioningStrings.InputsSecondaryButtonText, inputOptions.SecondaryButtonText);
 
        inputs.CompletionTcs.SetResult(InteractionResult.Cancel<InteractionInputCollection>());
 
        var repeatedNotification = await testInteractionService.Interactions.Reader.ReadAsync().AsTask().WaitAsync(s_testSynchronizationTimeout);
 
        Assert.Equal(AzureProvisioningStrings.NotificationTitle, repeatedNotification.Title);
        Assert.Equal(AzureProvisioningStrings.NotificationMessage, repeatedNotification.Message);
    }
 
    [Fact]
    public async Task EnsureProvisionedAsync_UsesControllerProvisioningFlow()
    {
        var builder = CreateBuilder(isRunMode: true);
        AddTestAzureProvisioning(builder);
 
        var storage = builder.AddAzureStorage("storage");
 
        using var app = builder.Build();
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
 
        await controller.EnsureProvisionedAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Running", storageEvent.Snapshot.State?.Text);
        Assert.Equal("https://storage.blob.core.windows.net/", storage.Resource.Outputs["blobEndpoint"]);
    }
 
    [Fact]
    public async Task EnsureProvisionedAsync_CompletesExistingPendingProvisioningWaiters()
    {
        var builder = CreateBuilder(isRunMode: true);
        var cachedStateProvisioner = new CachedStateTestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: cachedStateProvisioner);
 
        var storage = builder.AddAzureStorage("storage");
        storage.AddBlobs("blobs");
 
        using var app = builder.Build();
 
        var existingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        storage.Resource.ProvisioningTaskCompletionSource = existingTcs;
        var outputTask = storage.GetOutput("blobEndpoint").GetValueAsync(CancellationToken.None).AsTask();
 
        Assert.False(outputTask.IsCompleted);
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        await controller.EnsureProvisionedAsync(model);
 
        Assert.Same(existingTcs, storage.Resource.ProvisioningTaskCompletionSource);
        Assert.True(existingTcs.Task.IsCompletedSuccessfully);
        Assert.Equal("https://storage.blob.core.windows.net/", await outputTask.WaitAsync(s_testSynchronizationTimeout));
    }
 
    [Fact]
    public async Task RunModeInitializeResource_ProvisionsAzureResourcesAfterPrepareStep()
    {
        var builder = CreateBuilder(isRunMode: true);
        AddTestAzureProvisioning(builder);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var (steps, pipelineContext) = await CreateAzureEnvironmentPipelineStepsAsync(environmentResource, model, app.Services);
        var prepareResourcesStep = Assert.Single(steps, step => step.Name == AzureEnvironmentResource.PrepareResourcesStepName);
        var beforeStartSteps = steps.Where(step => step.RequiredBySteps.Contains(WellKnownPipelineSteps.BeforeStart)).ToArray();
 
        Assert.Collection(beforeStartSteps, step => Assert.Equal(AzureEnvironmentResource.PrepareResourcesStepName, step.Name));
        Assert.DoesNotContain(WellKnownPipelineSteps.BeforeStart, steps.Single(step => step.Name == AzureEnvironmentResource.ProvisionInfrastructureStepName).RequiredBySteps);
 
        await using var reportingStep = await new NullPublishingActivityReporter().CreateStepAsync("test");
        var stepContext = new PipelineStepContext
        {
            PipelineContext = pipelineContext,
            ReportingStep = reportingStep
        };
 
        await prepareResourcesStep.Action(stepContext);
 
        Assert.Empty(storage.Resource.Outputs);
 
        var eventing = app.Services.GetRequiredService<IDistributedApplicationEventing>();
        await eventing.PublishAsync(new InitializeResourceEvent(
            environmentResource,
            eventing,
            app.Services.GetRequiredService<ResourceLoggerService>(),
            notifications,
            app.Services));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.Running, storageEvent.Snapshot.State?.Text);
        Assert.Equal("https://storage.blob.core.windows.net/", storage.Resource.Outputs["blobEndpoint"]);
    }
 
    [Fact]
    public async Task EnsureProvisioned_UsesCachedStateWhenMissingResourceProbeCannotAuthenticate()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var cachedStateProvisioner = new CachedStateTestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder, armClientProvider: new CredentialUnavailableArmClientProvider(), bicepProvisioner: cachedStateProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """
            {
              // Cached output IDs are used to decide whether provisioning can be skipped.
              "id": {
                "value": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/storage",
              },
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.EnsureProvisionedAsync(model);
 
        Assert.Equal(1, cachedStateProvisioner.ConfigureResourceCallCount);
        Assert.Equal(0, cachedStateProvisioner.GetOrCreateResourceCallCount);
        Assert.Equal("https://storage.blob.core.windows.net/", storage.Resource.Outputs["blobEndpoint"]);
    }
 
    [Fact]
    public async Task EnsureProvisioned_UsesCachedStateWhenMissingResourceProbeFailsTransiently()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var cachedStateProvisioner = new CachedStateTestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder, armClientProvider: new ThrowingResourceProbeArmClientProvider(new RequestFailedException(503, "Service unavailable.")), bicepProvisioner: cachedStateProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"id":{"value":"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/storage"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.EnsureProvisionedAsync(model);
 
        Assert.Equal(1, cachedStateProvisioner.ConfigureResourceCallCount);
        Assert.Equal(0, cachedStateProvisioner.GetOrCreateResourceCallCount);
        Assert.Equal("https://storage.blob.core.windows.net/", storage.Resource.Outputs["blobEndpoint"]);
    }
 
    [Fact]
    public async Task OnBeforeStartAsync_AddsPerResourceCommandsToDeployableAzureResourcesOnly()
    {
        var builder = CreateBuilder(isRunMode: true);
        AddTestAzureProvisioning(builder);
 
        var storage = builder.AddAzureStorage("storage");
        var blobs = storage.AddBlobs("blobs");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        Assert.Contains(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ForgetStateCommandName);
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
        Assert.Contains("permanently lost", changeLocationCommand.DisplayDescription, StringComparison.Ordinal);
        Assert.Collection(
            changeLocationCommand.Arguments,
            locationArgument =>
            {
                Assert.Equal(AzureBicepResource.KnownParameters.Location, locationArgument.Name);
                Assert.True(locationArgument.Required);
                Assert.True(locationArgument.Disabled);
            },
            confirmDeleteArgument =>
            {
                Assert.Equal(AzureProvisioningController.ConfirmDeleteArgumentName, confirmDeleteArgument.Name);
                Assert.Equal(InputType.Boolean, confirmDeleteArgument.InputType);
                Assert.Equal("false", confirmDeleteArgument.Value);
                Assert.Contains("permanently lost", confirmDeleteArgument.Description, StringComparison.Ordinal);
            });
        Assert.Contains(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
        var deleteCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        Assert.True(cancelCommand.IsHighlighted);
        Assert.True(deleteCommand.IsHighlighted);
        Assert.True(reprovisionCommand.IsHighlighted);
        Assert.DoesNotContain(blobs.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c =>
            c.Name == AzureProvisioningController.ForgetStateCommandName ||
            c.Name == AzureProvisioningController.CancelCommandName ||
            c.Name == AzureProvisioningController.DeleteAzureResourceCommandName ||
            c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_IsHiddenForKeyVaultResources()
    {
        var builder = CreateBuilder(isRunMode: true);
        AddTestAzureProvisioning(builder);
 
        var keyVault = builder.AddAzureKeyVault("kv");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(keyVault.Resource, state => state with { State = KnownResourceStates.Running });
 
        Assert.True(notifications.TryGetCurrentState(keyVault.Resource.Name, out var keyVaultEvent));
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.ChangeResourceLocationCommandName, ResourceCommandState.Hidden);
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.GetAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Hidden);
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.DeleteAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.ForgetStateCommandName, ResourceCommandState.Enabled);
        AssertCommandState(keyVaultEvent.Snapshot, AzureProvisioningController.ReprovisionResourceCommandName, ResourceCommandState.Enabled);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_IsEnabledForResourcesWithImplicitKeyVaultChildren()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var postgres = builder.AddAzurePostgresFlexibleServer("pg")
            .WithPasswordAuthentication();
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(postgres.Resource, state => state with { State = KnownResourceStates.Running });
 
        Assert.True(notifications.TryGetCurrentState(postgres.Resource.Name, out var postgresEvent));
        AssertCommandState(postgresEvent.Snapshot, AzureProvisioningController.ChangeResourceLocationCommandName, ResourceCommandState.Enabled);
 
        var changeLocationCommand = Assert.Single(postgres.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = postgres.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments((AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.True(result.Success);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["pg"]);
        Assert.DoesNotContain("pg-kv", testBicepProvisioner.ProvisionedLocations.Keys);
 
        var data = AssertCommandJsonData(result);
        Assert.Equal(1, data["resourceCount"]?.GetValue<int>());
    }
 
    [Fact]
    public async Task GetAzureResourceCommand_ReturnsCachedDeploymentStateAndLiveStatus()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string tenantId = "87654321-4321-4321-4321-210987654321";
        const string resourceGroup = "test-rg";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/storage";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId]), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = subscriptionId;
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = resourceGroup;
        azureSection.Data["TenantId"] = tenantId;
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = deploymentId;
        storageSection.Data["Parameters"] = """
            {
              // Cached deployment state can be hand-edited while recovering local state.
              "location": { "value": "westus2", },
              "administratorLoginPassword": { "value": "P@ssw0rd123456789!" }
            }
            """;
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "type": "String",
                "value": "{{resourceId}}",
              },
              "blobEndpoint": {
                "type": "String",
                "value": "https://storage.blob.core.windows.net/",
              },
            }
            """;
        storageSection.Data["Scope"] = $$"""
            {
              "resourceGroup": "{{resourceGroup}}",
            }
            """;
        storageSection.Data["CheckSum"] = "checksum";
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        storageSection.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var getResourceCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
 
        var result = await getResourceCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource information retrieved.", result.Message);
 
        var data = AssertCommandJsonData(result);
        Assert.Equal(AzureProvisioningController.GetAzureResourceCommandName, data["command"]?.GetValue<string>());
        Assert.Equal("storage", data["resourceName"]?.GetValue<string>());
        Assert.Equal("westus2", data["azureLocation"]?.GetValue<string>());
        Assert.Equal("westus3", data["location"]?.GetValue<string>());
        Assert.Equal("westus3", data["effectiveLocation"]?.GetValue<string>());
        Assert.NotNull(result.Data);
        Assert.True(result.Data!.DisplayImmediately);
 
        var azureContext = Assert.IsType<JsonObject>(data["azureContext"]);
        Assert.Equal(subscriptionId, azureContext["subscriptionId"]?.GetValue<string>());
        Assert.Equal(tenantId, azureContext["tenantId"]?.GetValue<string>());
        Assert.Equal(resourceGroup, azureContext["resourceGroup"]?.GetValue<string>());
        Assert.Equal("westus2", azureContext["location"]?.GetValue<string>());
 
        var deployment = Assert.IsType<JsonObject>(data["deployment"]);
        Assert.True(deployment["hasState"]?.GetValue<bool>());
        Assert.Equal(deploymentId, deployment["deploymentId"]?.GetValue<string>());
        Assert.Equal(resourceId, deployment["resourceId"]?.GetValue<string>());
        Assert.Equal("westus3", deployment["locationOverride"]?.GetValue<string>());
        Assert.Equal("checksum", deployment["checksum"]?.GetValue<string>());
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateRunning, deployment["provisioningState"]?.GetValue<string>());
        Assert.Contains("/DeploymentDetailsBlade/", deployment["deploymentPortalUrl"]?.GetValue<string>());
        Assert.Contains("/resource/subscriptions/", deployment["resourcePortalUrl"]?.GetValue<string>());
 
        var outputs = Assert.IsType<JsonObject>(deployment["outputs"]);
        Assert.Equal("https://storage.blob.core.windows.net/", outputs["blobEndpoint"]?["value"]?.GetValue<string>());
        Assert.False(deployment.ContainsKey("parameters"));
        var scope = Assert.IsType<JsonObject>(deployment["scope"]);
        Assert.Equal(resourceGroup, scope["resourceGroup"]?.GetValue<string>());
 
        var live = Assert.IsType<JsonObject>(data["live"]);
        Assert.True(live["checked"]?.GetValue<bool>());
        Assert.True(live["exists"]?.GetValue<bool>());
    }
 
    [Fact]
    public async Task GetAzureResourceCommand_ReturnsMissingLiveResourceReasonWhenCachedResourceDoesNotExist()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(Array.Empty<string>()), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var getResourceCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
 
        var result = await getResourceCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
 
        var data = AssertCommandJsonData(result);
        var live = Assert.IsType<JsonObject>(data["live"]);
        Assert.True(live["checked"]?.GetValue<bool>());
        Assert.False(live["exists"]?.GetValue<bool>());
        Assert.Equal("missing-live-resource", live["reason"]?.GetValue<string>());
        Assert.Equal("azure", live["source"]?.GetValue<string>());
        Assert.Equal("missing-live-resource", live["code"]?.GetValue<string>());
        Assert.Contains("resource was not found in Azure", live["message"]?.GetValue<string>(), StringComparison.Ordinal);
        var recommendedActions = Assert.IsType<JsonArray>(live["recommendedActions"]);
        Assert.Contains(recommendedActions, action => action?["code"]?.GetValue<string>() == "reprovision-or-forget-state");
    }
 
    [Theory]
    [InlineData(AzureProvisioningFailureDetails.MissingLiveResourceReason, "reprovision-or-forget-state")]
    [InlineData(AzureProvisioningFailureDetails.KeyVaultDeletedStateTombstoneNotFoundReason, "retry-reprovision")]
    [InlineData(AzureProvisioningFailureDetails.ResourceGroupBeingDeletedErrorCode, "change-resource-group")]
    [InlineData(AzureProvisioningFailureDetails.SubscriptionNotFoundErrorCode, "change-subscription")]
    [InlineData(AzureProvisioningFailureDetails.ServiceModelDeprecatedErrorCode, "choose-supported-model-version")]
    [InlineData(AzureProvisioningFailureDetails.InvalidResourcePropertiesErrorCode, "fix-resource-properties")]
    public void AzureProvisioningFailureDetails_ReturnsKnownRecommendedActions(string errorCodeOrReason, string expectedActionCode)
    {
        var actions = AzureProvisioningFailureDetails.GetRecommendedActions(errorCodeOrReason);
 
        Assert.Contains(actions, action => action.Code == expectedActionCode);
    }
 
    [Fact]
    public void AzureProvisioningFailureDetails_ParsesDocumentedDeploymentOperationErrorShape()
    {
        const string targetResourceId =
            "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
        var response = new AzureProvisioningFailureTestResponse(400, "Bad Request",
            $$"""
            {
              "properties": {
                "statusCode": "BadRequest",
                "statusMessage": {
                  "error": {
                    "code": "InvalidAccountType",
                    "message": "The AccountType Standard_LRS1 is invalid. For more information, see - https://aka.ms/storageaccountskus"
                  }
                },
                "targetResource": {
                  "id": "{{targetResourceId}}",
                  "resourceType": "Microsoft.Storage/storageAccounts",
                  "resourceName": "storage"
                }
              }
            }
            """);
 
        var details = AzureProvisioningFailureDetails.FromRequestFailedException(
            new RequestFailedException(response),
            AzureProvisioningFailureDetails.ProvisionOperation);
 
        Assert.Equal("Microsoft.Storage", details.Provider);
        Assert.Equal("Microsoft.Storage/storageAccounts", details.ResourceType);
        Assert.Equal("storage", details.ResourceName);
        Assert.Equal(targetResourceId, details.TargetResourceId);
        Assert.Equal("InvalidAccountType", details.ErrorCode);
        Assert.Contains("Standard_LRS1", details.ErrorMessage, StringComparison.Ordinal);
 
        var json = details.ToJsonObject();
        Assert.Equal("Microsoft.Storage/storageAccounts", json["resourceType"]?.GetValue<string>());
        Assert.Equal("storage", json["resourceName"]?.GetValue<string>());
        Assert.Equal(targetResourceId, json["targetResourceId"]?.GetValue<string>());
 
        var properties = details.SetResourceProperties([], AzureProvisioningFailureDetails.ProvisionOperation);
        Assert.All(
            properties.Where(property => AzureProvisioningFailureDetails.IsFailureProperty(property.Name)),
            property =>
            {
                Assert.True(property.IsHighlighted);
                Assert.False(string.IsNullOrWhiteSpace(property.DisplayName));
            });
 
        var resourceNameProperty = properties.Single(property => property.Name == "azure.provisioning.error.resource.name");
        Assert.Equal(
            "storage",
            resourceNameProperty.Value?.ToString());
        Assert.Equal(AzureProvisioningStrings.FailurePropertyResourceNameDisplayName, resourceNameProperty.DisplayName);
 
        var targetResourceIdProperty = properties.Single(property => property.Name == "azure.provisioning.error.target.resource.id");
        Assert.Equal(
            targetResourceId,
            targetResourceIdProperty.Value?.ToString());
        Assert.Equal(AzureProvisioningStrings.FailurePropertyTargetResourceIdDisplayName, targetResourceIdProperty.DisplayName);
    }
 
    [Fact]
    public void AzureProvisioningFailureDetails_PromotesNestedProviderErrorAndKeepsTargetResource()
    {
        const string targetResourceId =
            "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/search";
        var response = new AzureProvisioningFailureTestResponse(400, "Bad Request",
            $$"""
            {
              "properties": {
                "targetResource": {
                  "id": "{{targetResourceId}}",
                  "resourceType": "Microsoft.Search/searchServices",
                  "resourceName": "search"
                },
                "statusMessage": {
                  "error": {
                    "code": "DeploymentFailed",
                    "message": "At least one resource deployment operation failed.",
                    "details": [
                      {
                        "code": "ResourceDeploymentFailure",
                        "message": "The resource write operation failed to complete successfully.",
                        "details": [
                          {
                            "code": "LocationNotAvailableForResourceType",
                            "message": "The provided location 'australiacentral' is not available for resource type 'Microsoft.Search/searchServices'.",
                            "target": "search"
                          }
                        ]
                      }
                    ]
                  }
                }
              }
            }
            """);
 
        var details = AzureProvisioningFailureDetails.FromRequestFailedException(
            new RequestFailedException(response),
            AzureProvisioningFailureDetails.ProvisionOperation);
 
        Assert.Equal("Microsoft.Search", details.Provider);
        Assert.Equal("Microsoft.Search/searchServices", details.ResourceType);
        Assert.Equal("search", details.ResourceName);
        Assert.Equal(targetResourceId, details.TargetResourceId);
        Assert.Equal(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, details.ErrorCode);
        Assert.Contains("australiacentral", details.ErrorMessage, StringComparison.Ordinal);
        Assert.Equal(AzureProvisioningFailureDetails.ProvisionOperation, details.Operation);
    }
 
    [Fact]
    public void AzureProvisioningFailureDetails_PromotesNestedResponseErrorAndKeepsTargetResource()
    {
        const string targetResourceId =
            "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Search/searchServices/search";
        var targetResource = new AzureDeploymentOperationTarget(
            targetResourceId,
            "Microsoft.Search/searchServices",
            "search");
        const string statusMessageContent = """
            {
              "status": "Failed",
              "error": {
                "code": "DeploymentFailed",
                "message": "At least one resource deployment operation failed.",
                "details": [
                  {
                    "code": "ResourceDeploymentFailure",
                    "message": "The resource write operation failed to complete successfully.",
                    "details": [
                      {
                        "code": "LocationNotAvailableForResourceType",
                        "message": "The provided location 'australiacentral' is not available for resource type 'Microsoft.Search/searchServices'.",
                        "target": "search"
                      }
                    ]
                  }
                ]
              }
            }
            """;
 
        var details = AzureProvisioningFailureDetails.FromResponseError(
            new ResponseError("DeploymentFailed", "At least one resource deployment operation failed."),
            targetResource,
            AzureProvisioningFailureDetails.ProvisionOperation,
            "BadRequest",
            "request-id",
            statusMessageContent);
 
        Assert.NotNull(details);
        Assert.Equal("Microsoft.Search", details.Provider);
        Assert.Equal("Microsoft.Search/searchServices", details.ResourceType);
        Assert.Equal("search", details.ResourceName);
        Assert.Equal(targetResourceId, details.TargetResourceId);
        Assert.Equal(AzureProvisioningFailureDetails.LocationNotAvailableForResourceTypeErrorCode, details.ErrorCode);
        Assert.Contains("australiacentral", details.ErrorMessage, StringComparison.Ordinal);
        Assert.Contains(details.RecommendedActions, action => action.Code == "change-location");
    }
 
    [Fact]
    public void WithoutAzureProvisioningFailureProperties_RemovesOnlyFailureProperties()
    {
        ImmutableArray<ResourcePropertySnapshot> properties =
        [
            new("azure.provisioning.error.code", "LocationNotAvailableForResourceType"),
            new("azure.provisioning.error.message", "old failure"),
            new("azure.subscription.id", "12345678-1234-1234-1234-123456789012"),
            new("custom.property", "kept")
        ];
 
        var filtered = properties.WithoutAzureProvisioningFailureProperties();
 
        Assert.Collection(
            filtered,
            property =>
            {
                Assert.Equal("azure.subscription.id", property.Name);
                Assert.Equal("12345678-1234-1234-1234-123456789012", property.Value);
            },
            property =>
            {
                Assert.Equal("custom.property", property.Name);
                Assert.Equal("kept", property.Value);
            });
    }
 
    [Fact]
    public void WithoutAzureProvisioningFailureProperties_ReturnsOriginalPropertiesWhenNoFailurePropertiesExist()
    {
        ImmutableArray<ResourcePropertySnapshot> properties =
        [
            new("azure.subscription.id", "12345678-1234-1234-1234-123456789012"),
            new("custom.property", "kept")
        ];
 
        var filtered = properties.WithoutAzureProvisioningFailureProperties();
 
        Assert.True(properties.Equals(filtered));
    }
 
    [Fact]
    public async Task GetAzureResourceCommand_ReturnsMissingResourceIdReasonWhenCachedStateHasNoOutputId()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"blobEndpoint":{"value":"https://storage.blob.core.windows.net/"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var getResourceCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
 
        var result = await getResourceCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.True(result.Data!.DisplayImmediately);
 
        var data = AssertCommandJsonData(result);
        var deployment = Assert.IsType<JsonObject>(data["deployment"]);
        Assert.True(deployment["hasState"]?.GetValue<bool>());
        Assert.Null(deployment["resourceId"]);
 
        var live = Assert.IsType<JsonObject>(data["live"]);
        Assert.False(live["checked"]?.GetValue<bool>());
        Assert.Null(live["exists"]);
        Assert.Equal("missing-resource-id", live["reason"]?.GetValue<string>());
        Assert.Equal("aspire", live["source"]?.GetValue<string>());
        Assert.Equal("missing-resource-id", live["code"]?.GetValue<string>());
        Assert.Contains("cached deployment state does not contain a resource ID", live["message"]?.GetValue<string>(), StringComparison.Ordinal);
        var recommendedActions = Assert.IsType<JsonArray>(live["recommendedActions"]);
        Assert.Contains(recommendedActions, action => action?["code"]?.GetValue<string>() == "reprovision-or-change-context");
    }
 
    [Fact]
    public async Task GetAzureResourceCommand_ReturnsStructuredRequestFailureWhenLiveProbeFails()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, armClientProvider: new ThrowingResourceProbeArmClientProvider(new RequestFailedException(403, "Forbidden", "AuthorizationFailed", null)), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var getResourceCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
 
        var result = await getResourceCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
 
        var data = AssertCommandJsonData(result);
        var live = Assert.IsType<JsonObject>(data["live"]);
        Assert.True(live["checked"]?.GetValue<bool>());
        Assert.Null(live["exists"]);
        Assert.Equal("request-failed", live["reason"]?.GetValue<string>());
        Assert.Equal("azure", live["source"]?.GetValue<string>());
        Assert.Equal(403, live["status"]?.GetValue<int>());
        Assert.Equal("Azure", live["provider"]?.GetValue<string>());
        Assert.Equal(403, live["httpStatus"]?.GetValue<int>());
        Assert.Equal("AuthorizationFailed", live["errorCode"]?.GetValue<string>());
        Assert.Equal("live-resource-check", live["operation"]?.GetValue<string>());
        Assert.Contains("Forbidden", live["message"]?.GetValue<string>(), StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAzureResourceCommand_ReturnsCredentialUnavailableReasonWhenLiveProbeCannotAuthenticate()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, armClientProvider: new CredentialUnavailableArmClientProvider(), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var getResourceCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.GetAzureResourceCommandName);
 
        var result = await getResourceCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
 
        var data = AssertCommandJsonData(result);
        var live = Assert.IsType<JsonObject>(data["live"]);
        Assert.True(live["checked"]?.GetValue<bool>());
        Assert.Null(live["exists"]);
        Assert.Equal("credential-unavailable", live["reason"]?.GetValue<string>());
        Assert.Contains("Credential unavailable", live["message"]?.GetValue<string>(), StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task CancelCommand_RetriesStateConflictAndMarksCachedDeploymentCanceled()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var canceledDeploymentIds = new List<string>();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: Array.Empty<string>(),
            deletedResourceIds: null,
            deploymentTargetResourceIds: null,
            canceledDeploymentIds: canceledDeploymentIds), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = deploymentId;
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(storageSection);
        deploymentStateManager.SaveConflictSectionName = "Azure:Deployments:storage";
        deploymentStateManager.SaveConflictCount = 1;
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = new("Waiting for Deployment", KnownResourceStateStyles.Info) });
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var result = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure cancellation requested.", result.Message);
        Assert.Equal([deploymentId], canceledDeploymentIds);
        Assert.Equal(1, deploymentStateManager.SaveConflictThrowCount);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Canceled", storageEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task CancelCommand_IsEnabledDuringActiveDeploymentOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var provisioningTask = controller.EnsureProvisionedAsync(model, CancellationToken.None);
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            provisioningTask,
            "Provisioning completed before the first resource started provisioning.");
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = new("Creating ARM Deployment", KnownResourceStateStyles.Info) });
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.ChangeResourceLocationCommandName, ResourceCommandState.Disabled);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.GetAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Enabled);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.DeleteAzureResourceCommandName, ResourceCommandState.Disabled);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.ForgetStateCommandName, ResourceCommandState.Disabled);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.ReprovisionResourceCommandName, ResourceCommandState.Disabled);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        await provisioningTask;
    }
 
    [Fact]
    public async Task CancelCommand_FastPathCancelsDuringActiveOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
        var canceledDeploymentIds = new List<string>();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.RemoveAll<IArmClientProvider>();
        builder.Services.RemoveAll<ITokenCredentialProvider>();
        builder.Services.AddSingleton<IArmClientProvider>(ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: Array.Empty<string>(),
            deletedResourceIds: null,
            deploymentTargetResourceIds: null,
            canceledDeploymentIds: canceledDeploymentIds));
        builder.Services.AddSingleton<ITokenCredentialProvider>(ProvisioningTestHelpers.CreateTokenCredentialProvider());
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data[BicepUtilities.DeploymentStateIdKey] = deploymentId;
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var provisioningTask = controller.EnsureProvisionedAsync(model, CancellationToken.None);
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            provisioningTask,
            "Provisioning completed before the first resource started provisioning.");
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = new(AzureProvisioningController.WaitingForDeploymentState, KnownResourceStateStyles.Info) });
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Enabled);
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var result = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(result.Success);
        Assert.Equal("Azure cancellation requested.", result.Message);
        Assert.Equal([deploymentId], canceledDeploymentIds);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateCanceled, storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => provisioningTask.WaitAsync(s_testSynchronizationTimeout));
    }
 
    [Fact]
    public async Task MutatingResourceCommands_FailFastDuringConflictingActiveOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var activeReprovisionTask = controller.ReprovisionResourceAsync(model, storage.Resource.Name, CancellationToken.None);
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            activeReprovisionTask,
            "Reprovision completed before the first resource started provisioning.");
 
        foreach (var commandName in new[]
        {
            AzureProvisioningController.ReprovisionResourceCommandName,
            AzureProvisioningController.DeleteAzureResourceCommandName,
            AzureProvisioningController.ForgetStateCommandName
        })
        {
            var command = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == commandName);
 
            var result = await command.ExecuteCommand(new ExecuteCommandContext
            {
                Services = app.Services,
                ResourceName = storage.Resource.Name,
                CancellationToken = CancellationToken.None,
                Logger = NullLogger.Instance,
                Arguments = new InteractionInputCollection([])
            }).WaitAsync(s_testSynchronizationTimeout);
 
            Assert.False(result.Success);
            Assert.Contains("already running or queued", result.Message, StringComparison.Ordinal);
        }
 
        Assert.False(activeReprovisionTask.IsCompleted);
        Assert.Equal([storage.Resource.Name], testBicepProvisioner.ProvisionedResources);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        Assert.True(await activeReprovisionTask.WaitAsync(s_testSynchronizationTimeout));
    }
 
    [Fact]
    public async Task MutatingResourceCommands_FailFastDuringConflictingQueuedOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage1 = builder.AddBicepTemplateString("storage1", "resource storage1 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var activeReprovisionTask = controller.ReprovisionResourceAsync(model, storage1.Resource.Name, CancellationToken.None);
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            activeReprovisionTask,
            "Reprovision completed before the first resource started provisioning.");
 
        var queuedReprovisionCommand = Assert.Single(storage2.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        var queuedReprovisionTask = queuedReprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage2.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(queuedReprovisionTask.IsCompleted);
 
        var conflictingCommand = Assert.Single(storage2.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
        var result = await conflictingCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage2.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.False(result.Success);
        Assert.Contains("already running or queued", result.Message, StringComparison.Ordinal);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        Assert.True(await activeReprovisionTask.WaitAsync(s_testSynchronizationTimeout));
        Assert.True((await queuedReprovisionTask.WaitAsync(s_testSynchronizationTimeout)).Success);
    }
 
    [Fact]
    public async Task CancelCommand_IsHiddenWhenResourceIsNotWaitingForDeployment()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Resources/deployments/storage";
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateSucceeded;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        var cancelCommand = Assert.Single(storageEvent.Snapshot.Commands, c => c.Name == AzureProvisioningController.CancelCommandName);
        Assert.Equal(ResourceCommandState.Hidden, cancelCommand.State);
    }
 
    [Fact]
    public async Task CancelCommand_DoesNotMarkCompletedDeploymentCanceled()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        AddTestAzureProvisioning(builder, armClientProvider: new CancelConflictArmClientProvider(), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = deploymentId;
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateSucceeded;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var result = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal(BicepProvisioner.DeploymentStateProvisioningStateSucceeded, storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey]?.GetValue<string>());
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.Running, storageEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task DeleteAzureResourceCommand_DeletesCachedOutputAndDeploymentOperationTargets()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var deletedResourceIds = new List<string>();
        var purgedDeletedKeyVaults = new List<(string ResourceId, string Location)>();
        var canceledDeploymentIds = new List<string>();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
        const string storageResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
        const string partialIdentityResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/storage-identity";
        const string nestedDeploymentResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/nested";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: [storageResourceId, partialIdentityResourceId, nestedDeploymentResourceId],
            deletedResourceIds: deletedResourceIds,
            deploymentTargetResourceIds: [partialIdentityResourceId, nestedDeploymentResourceId],
            canceledDeploymentIds: canceledDeploymentIds,
            purgedDeletedKeyVaults: purgedDeletedKeyVaults),
            deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = deploymentId;
        storageSection.Data["Outputs"] = new JsonObject
        {
            ["id"] = new JsonObject
            {
                ["type"] = "String",
                ["value"] = storageResourceId
            }
        }.ToJsonString();
        storageSection.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Id"] = "storage2-deployment";
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        storage.Resource.Outputs["blobEndpoint"] = "https://storage.blob.core.windows.net/";
        storage2.Resource.Outputs["blobEndpoint"] = "https://storage2.blob.core.windows.net/";
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = new("Failed to Provision", KnownResourceStateStyles.Error) });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var deleteCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resources deleted and provisioning state reset.", result.Message);
        Assert.Equal([deploymentId], canceledDeploymentIds);
        Assert.Equal(2, deletedResourceIds.Count);
        Assert.Contains(storageResourceId, deletedResourceIds);
        Assert.Contains(partialIdentityResourceId, deletedResourceIds);
        Assert.DoesNotContain(nestedDeploymentResourceId, deletedResourceIds);
        Assert.Empty(purgedDeletedKeyVaults);
 
        var resultData = AssertCommandJsonData(result);
        Assert.Equal(2, resultData["deletedResourceCount"]?.GetValue<int>());
        var resultResourceIds = Assert.IsType<JsonArray>(resultData["deletedResourceIds"]);
        Assert.Equal(deletedResourceIds, resultResourceIds.Select(static resourceId => resourceId!.GetValue<string>()));
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus3", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
        Assert.False(storageSection.Data.ContainsKey("Id"));
        Assert.False(storageSection.Data.ContainsKey("Outputs"));
 
        storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("storage2-deployment", storage2Section.Data["Id"]?.GetValue<string>());
 
        Assert.Empty(storage.Resource.Outputs);
        Assert.Equal("https://storage2.blob.core.windows.net/", storage2.Resource.Outputs["blobEndpoint"]);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.NotStarted, storageEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        Assert.Equal(KnownResourceStates.Running, storage2Event.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task DeleteAzureResourceCommand_SucceedsWhenKeyVaultPurgeTimesOutAfterDelete()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var deletedResourceIds = new List<string>();
        var purgedDeletedKeyVaults = new List<(string ResourceId, string Location)>();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/kv";
        const string keyVaultResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/kv-test";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: [keyVaultResourceId],
            deletedResourceIds: deletedResourceIds,
            deploymentTargetResourceIds: null,
            canceledDeploymentIds: null,
            purgedDeletedKeyVaults: purgedDeletedKeyVaults,
            purgeDeletedKeyVaultException: new TimeoutException("Timed out waiting for deleted Azure Key Vault to be purged.")),
            deploymentStateManager: deploymentStateManager);
 
        var keyVault = builder.AddBicepTemplateString("kv", "resource kv 'Microsoft.KeyVault/vaults@2024-11-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var keyVaultSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:kv");
        keyVaultSection.Data["Id"] = deploymentId;
        keyVaultSection.Data["Outputs"] = new JsonObject
        {
            ["id"] = new JsonObject
            {
                ["type"] = "String",
                ["value"] = keyVaultResourceId
            }
        }.ToJsonString();
        keyVaultSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(keyVaultSection);
 
        await notifications.PublishUpdateAsync(keyVault.Resource, state => state with { State = new("Failed to Provision", KnownResourceStateStyles.Error) });
 
        var deleteCommand = Assert.Single(keyVault.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = keyVault.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal([keyVaultResourceId], deletedResourceIds);
        var purgedDeletedKeyVault = Assert.Single(purgedDeletedKeyVaults);
        Assert.Equal(keyVaultResourceId, purgedDeletedKeyVault.ResourceId);
        Assert.Equal("westus2", purgedDeletedKeyVault.Location);
 
        var resultData = AssertCommandJsonData(result);
        Assert.Equal(1, resultData["deletedResourceCount"]?.GetValue<int>());
 
        keyVaultSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:kv");
        Assert.False(keyVaultSection.Data.ContainsKey("Id"));
        Assert.False(keyVaultSection.Data.ContainsKey("Outputs"));
 
        Assert.True(notifications.TryGetCurrentState(keyVault.Resource.Name, out var keyVaultEvent));
        Assert.Equal(KnownResourceStates.NotStarted, keyVaultEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task DeleteAzureResourceCommand_UpdatesCommandStatesWhileOperationIsActive()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var armClient = new BlockingDeleteArmClient();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        AddTestAzureProvisioning(builder, armClientProvider: new BlockingDeleteArmClientProvider(armClient), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var deleteCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
 
        var commandTask = deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            armClient.DeleteStarted.Task,
            commandTask,
            "Delete command completed before the ARM delete operation started.");
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Disabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Deleting", storageEvent.Snapshot.State?.Text);
        AssertAffectedResourceCommandsDuringOperation(storageEvent.Snapshot);
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        AssertUnaffectedResourceCommandsDuringOperation(storage2Event.Snapshot);
 
        armClient.AllowDeleteToComplete.TrySetResult();
        var result = await commandTask;
 
        Assert.True(result.Success);
        Assert.Equal([resourceId], armClient.DeletedResourceIds);
    }
 
    [Fact]
    public async Task DeleteAzureResourceCommand_PublishesCanceledWhenOperationIsCanceled()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var armClient = new BlockingDeleteArmClient();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string resourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Storage/storageAccounts/storage";
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<IArmClientProvider>();
        builder.Services.RemoveAll<ITokenCredentialProvider>();
        builder.Services.AddSingleton<IArmClientProvider>(new BlockingDeleteArmClientProvider(armClient));
        builder.Services.AddSingleton<ITokenCredentialProvider>(ProvisioningTestHelpers.CreateTokenCredentialProvider());
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var deleteCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourceCommandName);
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var deleteTask = deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            armClient.DeleteStarted.Task,
            deleteTask,
            "Delete command completed before the ARM delete operation started.");
 
        var cancelResult = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(cancelResult.Success);
 
        var deleteResult = await deleteTask;
        Assert.True(deleteResult.Canceled);
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Canceled", storageEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task ForgetStateCommand_ClearsOnlyTargetedResourceStateAndSnapshots()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Id"] = "storage2-deployment";
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        storage.Resource.Outputs["blobEndpoint"] = "https://storage.blob.core.windows.net/";
        storage2.Resource.Outputs["blobEndpoint"] = "https://storage2.blob.core.windows.net/";
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Urls = [new("deployment", "https://portal.azure.com/storage", false)],
            Properties = [new(CustomResourceKnownProperties.Source, "storage-deployment"), new("custom.property", "keep-storage")]
        });
 
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Urls = [new("deployment", "https://portal.azure.com/storage2", false)],
            Properties = [new(CustomResourceKnownProperties.Source, "storage2-deployment"), new("custom.property", "keep-storage2")]
        });
 
        var forgetCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ForgetStateCommandName);
 
        var result = await forgetCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource provisioning state reset.", result.Message);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
 
        storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("storage2-deployment", storage2Section.Data["Id"]?.GetValue<string>());
 
        Assert.Empty(storage.Resource.Outputs);
        Assert.Equal("https://storage2.blob.core.windows.net/", storage2.Resource.Outputs["blobEndpoint"]);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.NotStarted, storageEvent.Snapshot.State?.Text);
        Assert.DoesNotContain(storageEvent.Snapshot.Properties, p => p.Name == CustomResourceKnownProperties.Source);
        Assert.Contains(storageEvent.Snapshot.Properties, p => p.Name == "custom.property");
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        Assert.Equal(KnownResourceStates.Running, storage2Event.Snapshot.State?.Text);
        Assert.Contains(storage2Event.Snapshot.Properties, p => p.Name == CustomResourceKnownProperties.Source);
    }
 
    [Fact]
    public async Task ReprovisionCommand_ReprovisionsOnlyTargetedResource()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Id"] = "storage2-deployment";
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource reprovisioning completed.", result.Message);
        Assert.Contains(storage.Resource.Outputs, output => output.Key == "blobEndpoint");
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
 
        storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("storage2-deployment", storage2Section.Data["Id"]?.GetValue<string>());
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Running", storageEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        Assert.Equal(KnownResourceStates.Running, storage2Event.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task ReprovisionCommand_UpdatesCommandStatesWhileOperationIsActive()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var commandTask = reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            commandTask,
            "Reprovision command completed before provisioning started.");
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Disabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        AssertAffectedResourceCommandsDuringOperation(storageEvent.Snapshot);
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        AssertUnaffectedResourceCommandsDuringOperation(storage2Event.Snapshot);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        var result = await commandTask;
 
        Assert.True(result.Success);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out environmentEvent));
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Enabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out storageEvent));
        AssertUnaffectedResourceCommandsDuringOperation(storageEvent.Snapshot);
    }
 
    [Fact]
    public async Task ReprovisionCommand_ReenablesCommandStatesWhenOperationFails()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingThrowingTestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var commandTask = reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            commandTask,
            "Reprovision command completed before provisioning started.");
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Disabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        AssertAffectedResourceCommandsDuringOperation(storageEvent.Snapshot);
 
        testBicepProvisioner.AllowFirstProvisionToThrow.TrySetResult();
        var result = await commandTask;
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out environmentEvent));
        Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Enabled, command.State));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out storageEvent));
        AssertUnaffectedResourceCommandsDuringOperation(storageEvent.Snapshot);
    }
 
    [Fact]
    public async Task QueuedOperation_CancelledDuringInitialCommandStateRefreshCompletesAndReenablesCommands()
    {
        var builder = CreateBuilder(isRunMode: true);
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner());
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync();
        var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
        var queuedOperation = AzureProvisioningController.CreateEnsureProvisionedQueuedOperationForTesting(model, completion, cts.Token);
 
        await controller.ProcessQueuedOperationForTesting(queuedOperation);
 
        await Assert.ThrowsAsync<TaskCanceledException>(() => completion.Task.WaitAsync(s_testSynchronizationTimeout));
        Assert.Equal(ResourceCommandState.Enabled, controller.GetEnvironmentCommandState());
    }
 
    [Fact]
    public async Task MutatingResourceCommands_ExecuteSequentiallyWhenInvokedConcurrently()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var reprovisionStorageCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        var reprovisionStorage2Command = Assert.Single(storage2.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var storageTask = reprovisionStorageCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            storageTask,
            "First reprovision command completed before provisioning started.");
 
        var storage2Task = reprovisionStorage2Command.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage2.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(storageTask.IsCompleted);
        Assert.False(storage2Task.IsCompleted);
        Assert.Equal(["storage"], testBicepProvisioner.ProvisionedResources);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
 
        var result = await storageTask;
        var result2 = await storage2Task;
 
        Assert.True(result.Success);
        Assert.True(result2.Success);
        Assert.Equal(["storage", "storage2"], testBicepProvisioner.ProvisionedResources);
    }
 
    [Fact]
    public async Task CancelCommand_FastPathCancelsActiveOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new CancellationIgnoringBlockingTestBicepProvisioner();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var storage = builder.AddAzureStorage("storage");
        var blobs = storage.AddBlobs("blobs");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var commandTask = reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            commandTask,
            "Reprovision command completed before provisioning started.");
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Enabled);
 
        var cancelResult = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(cancelResult.Success);
        Assert.Equal("Azure cancellation requested.", cancelResult.Message);
        await testBicepProvisioner.OperationCancellationRequested.Task.WaitAsync(s_testSynchronizationTimeout);
        Assert.False(commandTask.IsCompleted);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out storageEvent));
        Assert.Equal("Canceling", storageEvent.Snapshot.State?.Text);
        AssertCommandState(storageEvent.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Disabled);
        Assert.True(notifications.TryGetCurrentState(blobs.Resource.Name, out var blobsEvent));
        Assert.Equal("Canceling", blobsEvent.Snapshot.State?.Text);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        var result = await commandTask.WaitAsync(s_testSynchronizationTimeout);
 
        Assert.False(result.Success);
        Assert.True(result.Canceled);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out storageEvent));
        Assert.Equal("Canceled", storageEvent.Snapshot.State?.Text);
        Assert.DoesNotContain(storageEvent.Snapshot.Properties, p => p.Name == AzureResourceProperties.OperationName);
    }
 
    [Fact]
    public async Task CancelCommand_DoesNotMarkCompletedSiblingsCancelingDuringAllResourceOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
        var provisioningTask = controller.EnsureProvisionedAsync(model, CancellationToken.None);
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            provisioningTask,
            "Provisioning completed before the first resource started provisioning.");
        await WaitForResourceStateAsync(notifications, storage2.Resource.Name, "Running");
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var runningStorage2Event));
        AssertCommandState(runningStorage2Event.Snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Hidden);
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var cancelResult = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(cancelResult.Success);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.True(
            storageEvent.Snapshot.State?.Text is "Canceling" or "Canceled",
            $"Expected storage to be canceling or canceled, but it was '{storageEvent.Snapshot.State?.Text}'.");
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        Assert.Equal("Running", storage2Event.Snapshot.State?.Text);
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => provisioningTask.WaitAsync(s_testSynchronizationTimeout));
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out storage2Event));
        Assert.Equal("Running", storage2Event.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task CancelCommand_FailsWhenNoOperationOrDeploymentIsActive()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
 
        var storage = builder.AddAzureStorage("storage");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var result = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Equal("No active cached Azure deployment was found for resource 'storage'.", result.Message);
    }
 
    [Fact]
    public async Task CancelCommand_DoesNotCancelUnaffectedActiveOperation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new CancellationIgnoringBlockingTestBicepProvisioner();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var storage = builder.AddAzureStorage("storage");
        var storage2 = builder.AddAzureStorage("storage2");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        var cancelCommand = Assert.Single(storage2.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
 
        var commandTask = reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            commandTask,
            "Reprovision command completed before provisioning started.");
 
        var cancelResult = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage2.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.False(cancelResult.Success);
        Assert.False(cancelResult.Canceled);
        Assert.Equal("No active cached Azure deployment was found for resource 'storage2'.", cancelResult.Message);
        Assert.False(testBicepProvisioner.OperationCancellationRequested.Task.IsCompleted);
        Assert.False(commandTask.IsCompleted);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        var result = await commandTask.WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(result.Success);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_UpdatesCommandStatesWhileOperationIsActive()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
        var operationStartedAt = new DateTimeOffset(2026, 6, 22, 12, 34, 56, TimeSpan.Zero);
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Services.AddSingleton<TimeProvider>(new FixedTimeProvider(operationStartedAt));
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var commandTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(("location", "westus2"))
        });
 
        try
        {
            await WaitForSignalBeforeOperationCompletesAsync(
                testBicepProvisioner.FirstProvisionStarted.Task,
                commandTask,
                "Change location command completed before command states were disabled.");
 
            Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
            Assert.All(environmentEvent.Snapshot.Commands, command => Assert.Equal(ResourceCommandState.Disabled, command.State));
 
            Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
            var storageSnapshot = storageEvent.Snapshot;
            AssertAffectedResourceCommandsDuringOperation(storageSnapshot);
            AssertHighlightedContextProperty(storageSnapshot.Properties, AzureResourceProperties.OperationName, "Change Azure resource location", AzureProvisioningStrings.OperationPropertyNameDisplayName);
            AssertHighlightedContextProperty(storageSnapshot.Properties, AzureResourceProperties.OperationPhase, "Reprovisioning", AzureProvisioningStrings.OperationPropertyPhaseDisplayName);
            AssertHighlightedContextProperty(storageSnapshot.Properties, AzureResourceProperties.OperationStatus, "Reprovisioning in westus2", AzureProvisioningStrings.OperationPropertyStatusDisplayName);
            AssertHighlightedContextProperty(storageSnapshot.Properties, AzureResourceProperties.OperationTargetLocation, "westus2", AzureProvisioningStrings.OperationPropertyTargetLocationDisplayName);
            AssertHighlightedContextProperty(storageSnapshot.Properties, AzureResourceProperties.OperationStartedAt, operationStartedAt.ToString("O"), AzureProvisioningStrings.OperationPropertyStartedAtDisplayName);
 
            Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
            AssertUnaffectedResourceCommandsDuringOperation(storage2Event.Snapshot);
            Assert.DoesNotContain(storage2Event.Snapshot.Properties, property => property.Name == AzureResourceProperties.OperationName);
        }
        finally
        {
            testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        }
 
        var result = await commandTask;
 
        Assert.True(result.Success);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_PersistsOverrideAndReprovisionsTargetedResource()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = new("Failed to Provision", KnownResourceStateStyles.Error) });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        interaction.Inputs[AzureBicepResource.KnownParameters.Location].Value = "westus2";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource location updated and reprovisioning completed.", result.Message);
        Assert.Equal("westus2", testBicepProvisioner.ProvisionedLocations["storage"]);
        Assert.DoesNotContain("storage2", testBicepProvisioner.ProvisionedLocations.Keys);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus2", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeLocationCommand_WithArguments_DoesNotPromptAndReturnsJsonResult()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments((AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource location updated and reprovisioning completed.", result.Message);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus3", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
 
        var data = AssertCommandJsonData(result);
        Assert.Equal(1, data["schemaVersion"]?.GetValue<int>());
        Assert.Equal(AzureProvisioningController.ChangeResourceLocationCommandName, data["command"]?.GetValue<string>());
        Assert.Equal("storage", data["resourceName"]?.GetValue<string>());
        Assert.Equal("westus3", data["location"]?.GetValue<string>());
        Assert.Equal("westus3", data["effectiveLocation"]?.GetValue<string>());
        Assert.Equal("eastus", data["azureLocation"]?.GetValue<string>());
        var azureContext = Assert.IsType<JsonObject>(data["azureContext"]);
        Assert.Equal("eastus", azureContext["location"]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeLocationCommand_ForAnnotatedResource_PersistsOverrideUnderBicepResourceName()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var visibleResource = new AnnotatedAzureResource("storage");
        var bicepResource = new AzureBicepResource("storage-deployment", templateString: "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        visibleResource.Annotations.Add(new AzureBicepResourceAnnotation(bicepResource));
        builder.AddResource(visibleResource);
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(visibleResource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(visibleResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = visibleResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments((AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.True(result.Success);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage-deployment"]);
 
        var bicepSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage-deployment");
        Assert.Equal("westus3", bicepSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
 
        var visibleSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.False(visibleSection.Data.ContainsKey(AzureProvisioningController.LocationOverrideKey));
    }
 
    [Fact]
    public async Task ChangeLocationCommand_UsesPersistedAzureContextForSelectableLocations()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testInteractionService = new TestInteractionService();
 
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "eastus";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
        var commandInputs = CloneInputs(changeLocationCommand.Arguments);
        var commandLocationInput = commandInputs[AzureBicepResource.KnownParameters.Location];
 
        await LoadInputAsync(app.Services, commandInputs, commandLocationInput);
 
        Assert.Equal("westus3", commandLocationInput.Value);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        var locationInput = interaction.Inputs[AzureBicepResource.KnownParameters.Location];
 
        Assert.Equal(InputType.Choice, locationInput.InputType);
        var options = Assert.IsAssignableFrom<IEnumerable<KeyValuePair<string, string>>>(locationInput.Options);
        Assert.Contains(options, option => option.Key == "westus2");
        Assert.Equal("westus3", locationInput.Value);
 
        locationInput.Value = "westus2";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource location updated and reprovisioning completed.", result.Message);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_RequiresConfirmationBeforeDeletingCachedResource()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var deletedResourceIds = new List<string>();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId], deletedResourceIds), bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = new JsonObject
        {
            ["id"] = new JsonObject
            {
                ["type"] = "String",
                ["value"] = resourceId
            }
        }.ToJsonString();
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments((AzureBicepResource.KnownParameters.Location, "westus3"))
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Contains(AzureProvisioningController.ConfirmDeleteArgumentName, result.Message, StringComparison.Ordinal);
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.Running, storageEvent.Snapshot.State?.Text);
        Assert.Empty(deletedResourceIds);
        Assert.DoesNotContain("storage", testBicepProvisioner.ProvisionedLocations.Keys);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.False(storageSection.Data.ContainsKey(AzureProvisioningController.LocationOverrideKey));
    }
 
    [Fact]
    public async Task ChangeLocationCommand_DeletesCachedResourceBeforeReprovisioningNewLocation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
        var deletedResourceIds = new List<string>();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId], deletedResourceIds), bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"id":{"value":"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        interaction.Inputs[AzureBicepResource.KnownParameters.Location].Value = "westus3";
        interaction.Inputs[AzureProvisioningController.ConfirmDeleteArgumentName].Value = "true";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.True(result.Success);
        Assert.Equal([resourceId], deletedResourceIds);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_DeletesCachedResourceUsingPersistedLocationWhenSnapshotLocationIsMissing()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var deletedResourceIds = new List<string>();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId], deletedResourceIds), bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Parameters"] = """
            {
              // The cached deployment location is used when the resource snapshot has not been published yet.
              "location": {
                "value": "westus2",
              },
            }
            """;
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}",
              },
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(
                (AzureBicepResource.KnownParameters.Location, "westus3"),
                (AzureProvisioningController.ConfirmDeleteArgumentName, "true"))
        });
 
        Assert.True(result.Success);
        Assert.Equal([resourceId], deletedResourceIds);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_UsesRequestedLocationWhenChangingExistingOverride()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
        var deletedResourceIds = new List<string>();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId], deletedResourceIds), bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["LocationOverride"] = "westus2";
        storageSection.Data["Outputs"] = """{"id":{"value":"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        interaction.Inputs[AzureBicepResource.KnownParameters.Location].Value = "westus3";
        interaction.Inputs[AzureProvisioningController.ConfirmDeleteArgumentName].Value = "true";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.True(result.Success);
        Assert.Equal([resourceId], deletedResourceIds);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus3", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeLocationCommand_TreatsDeletedCachedResourceAsAlreadyAbsent()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, armClientProvider: new DeleteResourceFailureArmClientProvider(resourceId, new RequestFailedException(404, "Not found.")), bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"id":{"value":"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        interaction.Inputs[AzureBicepResource.KnownParameters.Location].Value = "westus3";
        interaction.Inputs[AzureProvisioningController.ConfirmDeleteArgumentName].Value = "true";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.True(result.Success);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus3", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeLocationCommand_DoesNotRequireConfirmationWhenCachedResourceIsAlreadyAbsent()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var deletedResourceIds = new List<string>();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage26wmkwq4f4li52";
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(
            builder,
            armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(Array.Empty<string>(), deletedResourceIds),
            bicepProvisioner: testBicepProvisioner,
            deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = new JsonObject
        {
            ["id"] = new JsonObject
            {
                ["type"] = "String",
                ["value"] = resourceId
            }
        }.ToJsonString();
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments((AzureBicepResource.KnownParameters.Location, "westus3"))
        });
 
        Assert.True(result.Success);
        Assert.Empty(deletedResourceIds);
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage"]);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("westus3", storageSection.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReprovisionAllCommand_PreservesAzureContextState()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Id"] = "storage2-deployment";
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionAllCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionAllCommandName);
 
        var result = await reprovisionAllCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure reprovisioning completed.", result.Message);
 
        azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.Equal("12345678-1234-1234-1234-123456789012", azureSection.Data["SubscriptionId"]?.GetValue<string>());
        Assert.Equal("westus2", azureSection.Data["Location"]?.GetValue<string>());
        Assert.Equal("test-rg", azureSection.Data["ResourceGroup"]?.GetValue<string>());
        Assert.Equal("87654321-4321-4321-4321-210987654321", azureSection.Data["TenantId"]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_WithArguments_PersistsContextAndReturnsJsonResult()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
        var testInteractionService = new TestInteractionService();
 
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
 
        var result = await changeContextCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(
                ("subscriptionId", "12345678-1234-1234-1234-123456789012"),
                ("resourceGroup", "cli-rg-é"),
                (AzureBicepResource.KnownParameters.Location, "West US 3"),
                ("tenantId", "87654321-4321-4321-4321-210987654321"))
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure context updated and resources reprovisioned.", result.Message);
        Assert.Contains("storage", testBicepProvisioner.ProvisionedResources);
        Assert.False(testInteractionService.Interactions.Reader.TryRead(out _));
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.Equal("12345678-1234-1234-1234-123456789012", azureSection.Data["SubscriptionId"]?.GetValue<string>());
        Assert.Equal("westus3", azureSection.Data["Location"]?.GetValue<string>());
        Assert.Equal("cli-rg-é", azureSection.Data["ResourceGroup"]?.GetValue<string>());
        Assert.Equal("87654321-4321-4321-4321-210987654321", azureSection.Data["TenantId"]?.GetValue<string>());
 
        var data = AssertCommandJsonData(result);
        Assert.Equal(1, data["schemaVersion"]?.GetValue<int>());
        Assert.Equal(AzureProvisioningController.ChangeAzureContextCommandName, data["command"]?.GetValue<string>());
        Assert.Equal("12345678-1234-1234-1234-123456789012", data["subscriptionId"]?.GetValue<string>());
        Assert.Equal("87654321-4321-4321-4321-210987654321", data["tenantId"]?.GetValue<string>());
        Assert.Equal("cli-rg-é", data["resourceGroup"]?.GetValue<string>());
        Assert.Equal("westus3", data["azureLocation"]?.GetValue<string>());
        var azureContext = Assert.IsType<JsonObject>(data["azureContext"]);
        Assert.Equal("12345678-1234-1234-1234-123456789012", azureContext["subscriptionId"]?.GetValue<string>());
        Assert.Equal("87654321-4321-4321-4321-210987654321", azureContext["tenantId"]?.GetValue<string>());
        Assert.Equal("cli-rg-é", azureContext["resourceGroup"]?.GetValue<string>());
        Assert.Equal("westus3", azureContext["location"]?.GetValue<string>());
        Assert.Contains("cli-rg-é", result.Data!.Value, StringComparison.Ordinal);
        Assert.DoesNotContain("\\u00E9", result.Data.Value, StringComparison.OrdinalIgnoreCase);
 
        Assert.Equal(1, data["resourceCount"]?.GetValue<int>());
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(KnownResourceStates.Running, environmentEvent.Snapshot.State?.Text);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.subscription.id", "12345678-1234-1234-1234-123456789012", AzureProvisioningStrings.ContextPropertySubscriptionIdDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.resource.group", "cli-rg-é", AzureProvisioningStrings.ContextPropertyResourceGroupDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.location", "westus3", AzureProvisioningStrings.ContextPropertyLocationDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.tenant.id", "87654321-4321-4321-4321-210987654321", AzureProvisioningStrings.ContextPropertyTenantIdDisplayName);
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_WithArgumentsWithoutTenant_ClearsPersistedTenant()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, deploymentStateManager: deploymentStateManager);
 
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
 
        var result = await changeContextCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(
                ("subscriptionId", "12345678-1234-1234-1234-123456789012"),
                ("resourceGroup", "cli-rg"),
                (AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.True(result.Success, result.Message);
 
        azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.False(azureSection.Data.ContainsKey("TenantId"));
 
        var data = AssertCommandJsonData(result);
        Assert.Null(data["tenantId"]);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.DoesNotContain(environmentEvent.Snapshot.Properties, p => p.Name == "azure.tenant.id");
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_FailsWhenLocationChangeWouldReprovisionKeyVault()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        builder.AddAzureKeyVault("kv");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
 
        var result = await changeContextCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(
                ("subscriptionId", "12345678-1234-1234-1234-123456789012"),
                ("resourceGroup", "test-rg"),
                (AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Equal(AzureProvisioningStrings.ChangeResourceLocationKeyVaultUnsupported, result.Message);
        Assert.Empty(testBicepProvisioner.ProvisionedResources);
 
        azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.Equal("westus2", azureSection.Data["Location"]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ChangeAzureContextCommand_DoesNotInferResourceLocationOverrides()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Parameters"] = """{"location":{"value":"westus2"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with
        {
            State = KnownResourceStates.Running,
            Properties = [new("azure.location", "westus2")]
        });
 
        var changeContextCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeAzureContextCommandName);
 
        var result = await changeContextCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = CreateArguments(
                ("subscriptionId", "12345678-1234-1234-1234-123456789012"),
                ("resourceGroup", "cli-rg"),
                (AzureBicepResource.KnownParameters.Location, "West US 3"))
        });
 
        Assert.True(result.Success, result.Message);
        Assert.NotEqual("westus2", testBicepProvisioner.ProvisionedLocations["storage"]);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.False(storageSection.Data.ContainsKey(AzureProvisioningController.LocationOverrideKey));
    }
 
    [Fact]
    public async Task ReprovisionAllCommand_NormalizesPersistedLocationOverride()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data[AzureProvisioningController.LocationOverrideKey] = "West US 3";
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        await controller.ReprovisionAllAsync(model);
 
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage2"]);
 
        storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("westus3", storage2Section.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReprovisionAllCommand_PreservesLocationOverrideFromPersistedParameters()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Parameters"] = """
            {
              // Preserve resource-specific locations from cached deployment parameters.
              "location": { "value": "westus3", },
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        await controller.ReprovisionAllAsync(model);
 
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage2"]);
 
        storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("westus3", storage2Section.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ReprovisionResourceCommand_PreservesInMemoryLocationOverrideWhenCachedStateIsMissing()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storage2 = model.Resources.OfType<AzureBicepResource>().Single(r => r.Name == "storage2");
        await notifications.PublishUpdateAsync(storage2, state => state with
        {
            State = KnownResourceStates.Running,
            Properties =
            [
                new("azure.location", "westus3"),
                new("azure.subscription.id", "12345678-1234-1234-1234-123456789012")
            ]
        });
 
        var reprovisionCommand = Assert.Single(storage2.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage2.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resource reprovisioning completed.", result.Message);
 
        Assert.Equal("westus3", testBicepProvisioner.ProvisionedLocations["storage2"]);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        Assert.Equal("westus3", storage2Section.Data[AzureProvisioningController.LocationOverrideKey]?.GetValue<string>());
    }
 
    [Fact]
    public async Task ForgetResourceStateCommand_ClearsInMemoryLocationParameter()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testBicepProvisioner = new TestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner, deploymentStateManager: deploymentStateManager);
 
        builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storage = model.Resources.OfType<AzureBicepResource>().Single(r => r.Name == "storage");
        storage.Parameters[AzureBicepResource.KnownParameters.Location] = "westus3";
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        storageSection.Data[AzureProvisioningController.LocationOverrideKey] = "westus3";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.ForgetResourceStateAsync(model, "storage");
 
        Assert.False(storage.Parameters.ContainsKey(AzureBicepResource.KnownParameters.Location));
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
    }
 
    [Fact]
    public async Task ReprovisionResourceCommand_FailsWhenProvisioningFails()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner());
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
    }
 
    [Fact]
    public async Task ReprovisionResourceCommand_PurgesDeletedKeyVaultAndRetriesWhenSoftDeleteConflictOccurs()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string keyVaultResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/kv-test";
        var deletedResourceIds = new List<string>();
        var purgedDeletedKeyVaults = new List<(string ResourceId, string Location)>();
        var testBicepProvisioner = new KeyVaultSoftDeleteConflictThenSuccessProvisioner(keyVaultResourceId);
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<IArmClientProvider>();
        builder.Services.AddSingleton<IArmClientProvider>(ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: [],
            deletedResourceIds: deletedResourceIds,
            deploymentTargetResourceIds: null,
            canceledDeploymentIds: null,
            purgedDeletedKeyVaults: purgedDeletedKeyVaults));
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var keyVault = builder.AddBicepTemplateString("kv3", "resource kv 'Microsoft.KeyVault/vaults@2024-11-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var keyVaultSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:kv3");
        keyVaultSection.Data[AzureProvisioningController.LocationOverrideKey] = "ukwest";
        await deploymentStateManager.SaveSectionAsync(keyVaultSection);
 
        await notifications.PublishUpdateAsync(keyVault.Resource, state => state with { State = new("Canceled", KnownResourceStateStyles.Info) });
 
        var reprovisionCommand = Assert.Single(keyVault.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = keyVault.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal(2, testBicepProvisioner.GetOrCreateResourceCallCount);
        Assert.Empty(deletedResourceIds);
        var purgedDeletedKeyVault = Assert.Single(purgedDeletedKeyVaults);
        Assert.Equal(keyVaultResourceId, purgedDeletedKeyVault.ResourceId);
        Assert.Equal("ukwest", purgedDeletedKeyVault.Location);
 
        Assert.True(notifications.TryGetCurrentState(keyVault.Resource.Name, out var keyVaultEvent));
        Assert.Equal("Running", keyVaultEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task ReprovisionResourceCommand_FailsWithDiagnosticWhenSoftDeleteConflictTombstoneIsNotDiscoverable()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string keyVaultResourceId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/kv-test";
        var deletedResourceIds = new List<string>();
        var purgedDeletedKeyVaults = new List<(string ResourceId, string Location)>();
        var testBicepProvisioner = new KeyVaultSoftDeleteConflictThenSuccessProvisioner(keyVaultResourceId);
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
        builder.Services.AddSingleton<IDeploymentStateManager>(deploymentStateManager);
        builder.AddAzureProvisioning();
        builder.Services.RemoveAll<IArmClientProvider>();
        builder.Services.AddSingleton<IArmClientProvider>(ProvisioningTestHelpers.CreateArmClientProvider(
            existingResourceIds: [],
            deletedResourceIds: deletedResourceIds,
            deploymentTargetResourceIds: null,
            canceledDeploymentIds: null,
            purgedDeletedKeyVaults: purgedDeletedKeyVaults,
            purgeDeletedKeyVaultResult: false));
        builder.Services.RemoveAll<AzureProvisioningController>();
        builder.Services.AddSingleton(sp => new AzureProvisioningController(
            sp.GetRequiredService<IConfiguration>(),
            sp.GetRequiredService<IOptions<AzureProvisionerOptions>>(),
            sp,
            testBicepProvisioner,
            deploymentStateManager,
            sp.GetRequiredService<IDistributedApplicationEventing>(),
            testProvisioningContextProvider,
            sp.GetRequiredService<IAzureProvisioningOptionsManager>(),
            sp.GetRequiredService<ResourceNotificationService>(),
            sp.GetRequiredService<ResourceLoggerService>(),
            sp.GetRequiredService<ILogger<AzureProvisioningController>>()));
 
        var keyVault = builder.AddBicepTemplateString("kv3", "resource kv 'Microsoft.KeyVault/vaults@2024-11-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var keyVaultSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:kv3");
        keyVaultSection.Data[AzureProvisioningController.LocationOverrideKey] = "ukwest";
        await deploymentStateManager.SaveSectionAsync(keyVaultSection);
 
        await notifications.PublishUpdateAsync(keyVault.Resource, state => state with { State = new("Canceled", KnownResourceStateStyles.Info) });
 
        var reprovisionCommand = Assert.Single(keyVault.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = keyVault.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Equal(1, testBicepProvisioner.GetOrCreateResourceCallCount);
        Assert.Empty(deletedResourceIds);
        Assert.Collection(
            purgedDeletedKeyVaults,
            purgedDeletedKeyVault =>
            {
                Assert.Equal(keyVaultResourceId, purgedDeletedKeyVault.ResourceId);
                Assert.Equal("ukwest", purgedDeletedKeyVault.Location);
            },
            purgedDeletedKeyVault =>
            {
                Assert.Equal(keyVaultResourceId, purgedDeletedKeyVault.ResourceId);
                Assert.Equal("westus2", purgedDeletedKeyVault.Location);
            });
 
        var data = AssertCommandJsonData(result);
        Assert.False(data["success"]?.GetValue<bool>());
        var diagnostics = Assert.IsType<JsonArray>(data["diagnostics"]);
        var diagnostic = Assert.IsType<JsonObject>(Assert.Single(diagnostics));
        Assert.Equal("Microsoft.KeyVault", diagnostic["provider"]?.GetValue<string>());
        Assert.Equal("Microsoft.KeyVault/vaults", diagnostic["resourceType"]?.GetValue<string>());
        Assert.Equal("kv-test", diagnostic["resourceName"]?.GetValue<string>());
        Assert.Equal(keyVaultResourceId, diagnostic["targetResourceId"]?.GetValue<string>());
        Assert.Equal(AzureProvisioningFailureDetails.KeyVaultDeletedStateTombstoneNotFoundReason, diagnostic["errorCode"]?.GetValue<string>());
        Assert.Equal(409, diagnostic["httpStatus"]?.GetValue<int>());
        Assert.Contains("deleted vault was not found", diagnostic["errorMessage"]?.GetValue<string>(), StringComparison.Ordinal);
        var recommendedActions = Assert.IsType<JsonArray>(diagnostic["recommendedActions"]);
        Assert.Contains(recommendedActions, action => action?["code"]?.GetValue<string>() == "retry-reprovision");
 
        Assert.True(notifications.TryGetCurrentState(keyVault.Resource.Name, out var keyVaultEvent));
        Assert.Equal("Failed to Provision", keyVaultEvent.Snapshot.State?.Text);
        Assert.Equal(AzureProvisioningFailureDetails.KeyVaultDeletedStateTombstoneNotFoundReason, keyVaultEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.code").Value?.ToString());
        var snapshotRecommendedActions = Assert.IsType<string[]>(keyVaultEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.recommendedActions").Value);
        Assert.Contains(snapshotRecommendedActions, action => action.Contains("deleted Key Vault tombstone", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task ReprovisionResourceCommand_ProvisionsImplicitKeyVaultBeforePasswordAuthenticatedPostgres()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new ParentChildOrderingBicepProvisioner("pg-kv", "pg");
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "test-rg";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var postgres = builder.AddAzurePostgresFlexibleServer("pg")
            .WithPasswordAuthentication();
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(postgres.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(postgres.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var commandTask = reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = postgres.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        await testBicepProvisioner.ChildProvisionStarted.Task.WaitAsync(s_testSynchronizationTimeout);
        Assert.DoesNotContain("pg", testBicepProvisioner.ProvisionedResources);
 
        testBicepProvisioner.AllowChildProvisionToComplete.TrySetResult();
        var result = await commandTask.WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(result.Success);
        Assert.False(testBicepProvisioner.ParentStartedBeforeChildCompleted);
        var provisionedResources = testBicepProvisioner.ProvisionedResources.ToArray();
        var childIndex = Array.IndexOf(provisionedResources, "pg-kv");
        var parentIndex = Array.IndexOf(provisionedResources, "pg");
        Assert.Equal(0, childIndex);
        Assert.NotEqual(-1, parentIndex);
        Assert.True(childIndex < parentIndex);
    }
 
    [Fact]
    public async Task ChangeLocationCommand_FailsWhenProvisioningFails()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testInteractionService = new TestInteractionService();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        builder.Services.AddSingleton<IInteractionService>(testInteractionService);
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner());
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var executionTask = changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        var interaction = await testInteractionService.Interactions.Reader.ReadAsync();
        interaction.Inputs[AzureBicepResource.KnownParameters.Location].Value = "westus2";
        interaction.CompletionTcs.SetResult(InteractionResult.Ok(interaction.Inputs));
 
        var result = await executionTask;
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ChangeLocationCommand_FailsForKeyVaultWithoutProvisioning(bool includeLocationArgument)
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new TestBicepProvisioner();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "eastus";
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var keyVault = builder.AddAzureKeyVault("kv");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(keyVault.Resource, state => state with { State = KnownResourceStates.Running });
 
        var changeLocationCommand = Assert.Single(keyVault.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ChangeResourceLocationCommandName);
 
        var result = await changeLocationCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = keyVault.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = includeLocationArgument
                ? CreateArguments((AzureBicepResource.KnownParameters.Location, "westus2"))
                : new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Equal(AzureProvisioningStrings.ChangeResourceLocationKeyVaultUnsupported, result.Message);
        Assert.Empty(testBicepProvisioner.ProvisionedResources);
    }
 
    [Fact]
    public async Task ReprovisionCommand_ReturnsStructuredProviderFailureDetailsWhenProvisioningFails()
    {
        var builder = CreateBuilder(isRunMode: true);
        var requestFailedException = new RequestFailedException(
            400,
            "The provided location 'asia' is not available for resource type 'Microsoft.ManagedIdentity/userAssignedIdentities'.",
            "LocationNotAvailableForResourceType",
            innerException: null);
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "asia";
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner(requestFailedException));
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.False(result.Canceled);
        Assert.Contains("Azure provisioning failed.", result.Message, StringComparison.Ordinal);
        Assert.Contains("LocationNotAvailableForResourceType", result.Message, StringComparison.Ordinal);
        Assert.Contains(AzureProvisioningController.ChangeResourceLocationCommandName, result.Message, StringComparison.Ordinal);
 
        var data = AssertCommandJsonData(result);
        Assert.False(data["success"]?.GetValue<bool>());
        Assert.Equal("failed", data["status"]?.GetValue<string>());
        var diagnostics = Assert.IsType<JsonArray>(data["diagnostics"]);
        var diagnostic = Assert.IsType<JsonObject>(Assert.Single(diagnostics));
        Assert.Equal("azure", diagnostic["source"]?.GetValue<string>());
        Assert.Equal("Microsoft.ManagedIdentity", diagnostic["provider"]?.GetValue<string>());
        Assert.Equal("Microsoft.ManagedIdentity/userAssignedIdentities", diagnostic["resourceType"]?.GetValue<string>());
        Assert.Equal(400, diagnostic["httpStatus"]?.GetValue<int>());
        Assert.Equal("LocationNotAvailableForResourceType", diagnostic["errorCode"]?.GetValue<string>());
        Assert.Equal("provision", diagnostic["operation"]?.GetValue<string>());
        var resultRecommendedActions = Assert.IsType<JsonArray>(diagnostic["recommendedActions"]);
        Assert.Contains(resultRecommendedActions, action => action?["code"]?.GetValue<string>() == "change-location");
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal("Failed to Provision", storageEvent.Snapshot.State?.Text);
        Assert.All(
            storageEvent.Snapshot.Properties.Where(property => AzureProvisioningFailureDetails.IsFailureProperty(property.Name)),
            property =>
            {
                Assert.True(property.IsHighlighted);
                Assert.False(string.IsNullOrWhiteSpace(property.DisplayName));
            });
        Assert.Equal("Microsoft.ManagedIdentity", storageEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.provider").Value?.ToString());
        Assert.Equal("Microsoft.ManagedIdentity/userAssignedIdentities", storageEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.resource.type").Value?.ToString());
        Assert.Equal("LocationNotAvailableForResourceType", storageEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.code").Value?.ToString());
        Assert.Equal(400, storageEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.http.status").Value);
        var snapshotRecommendedActions = Assert.IsType<string[]>(storageEvent.Snapshot.Properties.Single(p => p.Name == "azure.provisioning.error.recommendedActions").Value);
        Assert.Contains(snapshotRecommendedActions, action => action.Contains(AzureProvisioningController.ChangeResourceLocationCommandName, StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task ResourceCommandCancellation_ReturnsCanceledResult()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        AddTestAzureProvisioning(builder);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var reprovisionCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.ReprovisionResourceCommandName);
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync();
 
        var result = await reprovisionCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = cts.Token,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.True(result.Canceled);
    }
 
    [Fact]
    public async Task CheckForDriftAsync_MarksResourceMissingInAzure()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(existingResourceIds: []), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        azureSection.Data["Tenant"] = "testdomain.onmicrosoft.com";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"id":{"value":"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        await controller.CheckForDriftAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(AzureProvisioningController.DriftedState, environmentEvent.Snapshot.State?.Text);
        Assert.Equal(KnownResourceStateStyles.Error, environmentEvent.Snapshot.State?.Style);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.subscription.id", "12345678-1234-1234-1234-123456789012", AzureProvisioningStrings.ContextPropertySubscriptionIdDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.resource.group", "test-rg", AzureProvisioningStrings.ContextPropertyResourceGroupDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.location", "westus2", AzureProvisioningStrings.ContextPropertyLocationDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.tenant.id", "87654321-4321-4321-4321-210987654321", AzureProvisioningStrings.ContextPropertyTenantIdDisplayName);
        AssertHighlightedContextProperty(environmentEvent.Snapshot.Properties, "azure.tenant.domain", "testdomain.onmicrosoft.com", AzureProvisioningStrings.ContextPropertyTenantDomainDisplayName);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var resourceEvent));
        Assert.Equal(AzureProvisioningController.MissingInAzureState, resourceEvent.Snapshot.State?.Text);
        Assert.Equal(KnownResourceStateStyles.Error, resourceEvent.Snapshot.State?.Style);
    }
 
    [Fact]
    public async Task CheckForDriftAsync_LeavesRunningResourcesWhenAzureResourcesStillExist()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string resourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([resourceId]), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{resourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        await controller.CheckForDriftAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(KnownResourceStates.Running, environmentEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var resourceEvent));
        Assert.Equal(KnownResourceStates.Running, resourceEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task CheckForDriftAsync_MarksOnlyMissingResourceWhenOtherAzureResourcesStillExist()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        const string existingResourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage";
        const string missingResourceId = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/storage2";
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider([existingResourceId]), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{existingResourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var storage2Section = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage2");
        storage2Section.Data["Outputs"] = $$"""
            {
              "id": {
                "value": "{{missingResourceId}}"
              }
            }
            """;
        await deploymentStateManager.SaveSectionAsync(storage2Section);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage2.Resource, state => state with { State = KnownResourceStates.Running });
 
        await controller.CheckForDriftAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(AzureProvisioningController.DriftedState, environmentEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(KnownResourceStates.Running, storageEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage2.Resource.Name, out var storage2Event));
        Assert.Equal(AzureProvisioningController.MissingInAzureState, storage2Event.Snapshot.State?.Text);
        Assert.Equal(KnownResourceStateStyles.Error, storage2Event.Snapshot.State?.Style);
    }
 
    [Fact]
    public async Task CheckForDriftAsync_SkipsResourcesWithoutCachedResourceIds()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(existingResourceIds: []), deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Outputs"] = """{"blobEndpoint":{"value":"https://storage.blob.core.windows.net/"}}""";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        await notifications.PublishUpdateAsync(environmentResource, state => state with { State = KnownResourceStates.Running });
        await notifications.PublishUpdateAsync(storage.Resource, state => state with { State = KnownResourceStates.Running });
 
        await controller.CheckForDriftAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal(KnownResourceStates.Running, environmentEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var resourceEvent));
        Assert.Equal(KnownResourceStates.Running, resourceEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task DeleteAzureResourcesCommand_DeletesCurrentResourceGroupAndPreservesAzureContextState()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var resourceGroup = new TestResourceGroupResource("test-rg");
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        AddTestAzureProvisioning(builder, armClientProvider: new TestArmClientProvider(resourceGroup), provisioningContextProvider: testProvisioningContextProvider, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        azureSection.Data["TenantId"] = "87654321-4321-4321-4321-210987654321";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var deleteCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourcesCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resources deleted and provisioning state reset.", result.Message);
        Assert.Equal(1, resourceGroup.DeleteCallCount);
        Assert.Equal(0, testProvisioningContextProvider.CreateProvisioningContextCallCount);
 
        azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        Assert.Equal("12345678-1234-1234-1234-123456789012", azureSection.Data["SubscriptionId"]?.GetValue<string>());
        Assert.Equal("westus2", azureSection.Data["Location"]?.GetValue<string>());
        Assert.Equal("test-rg", azureSection.Data["ResourceGroup"]?.GetValue<string>());
        Assert.Equal("87654321-4321-4321-4321-210987654321", azureSection.Data["TenantId"]?.GetValue<string>());
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
 
        Assert.Empty(storage.Resource.Outputs);
    }
 
    [Fact]
    public async Task DeleteAzureResourcesCommand_DoesNotDeleteConfiguredResourceGroupWhenPersistedContextIsMissing()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var resourceGroup = new TestResourceGroupResource("configured-rg");
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        builder.Configuration["Azure:SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = "configured-rg";
        AddTestAzureProvisioning(builder, armClientProvider: new TestArmClientProvider(resourceGroup), provisioningContextProvider: testProvisioningContextProvider, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var deleteCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourcesCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resources deleted and provisioning state reset.", result.Message);
        Assert.Equal(0, resourceGroup.DeleteCallCount);
        Assert.Equal(0, testProvisioningContextProvider.CreateProvisioningContextCallCount);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
        Assert.Empty(storage.Resource.Outputs);
    }
 
    [Fact]
    public async Task DeleteAzureResourcesCommand_TreatsMissingResourceGroupAsSuccessAndClearsState()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
 
        AddTestAzureProvisioning(builder, armClientProvider: ProvisioningTestHelpers.CreateArmClientProviderForMissingResourceGroup(), provisioningContextProvider: testProvisioningContextProvider, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "missing-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var deleteCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourcesCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.True(result.Success);
        Assert.Equal("Azure resources deleted and provisioning state reset.", result.Message);
        Assert.Equal(0, testProvisioningContextProvider.CreateProvisioningContextCallCount);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Empty(storageSection.Data);
        Assert.Empty(storage.Resource.Outputs);
    }
 
    [Fact]
    public async Task DeleteAzureResourcesCommand_PublishesFailureWhenResourceGroupDeleteFails()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var testProvisioningContextProvider = new TestProvisioningContextProvider();
        var resourceGroup = new TestResourceGroupResource("test-rg", new RequestFailedException(409, "Resource group is locked."));
 
        AddTestAzureProvisioning(builder, armClientProvider: new TestArmClientProvider(resourceGroup), provisioningContextProvider: testProvisioningContextProvider, deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var environmentResource = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
 
        var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure");
        azureSection.Data["SubscriptionId"] = "12345678-1234-1234-1234-123456789012";
        azureSection.Data["Location"] = "westus2";
        azureSection.Data["ResourceGroup"] = "test-rg";
        await deploymentStateManager.SaveSectionAsync(azureSection);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data["Id"] = "storage-deployment";
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var deleteCommand = Assert.Single(environmentResource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.DeleteAzureResourcesCommandName);
 
        var result = await deleteCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = environmentResource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        });
 
        Assert.False(result.Success);
        Assert.Equal(1, resourceGroup.DeleteCallCount);
        Assert.Equal(0, testProvisioningContextProvider.CreateProvisioningContextCallCount);
 
        Assert.True(notifications.TryGetCurrentState(environmentResource.Name, out var environmentEvent));
        Assert.Equal("Failed to Delete", environmentEvent.Snapshot.State?.Text);
 
        storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        Assert.Equal("storage-deployment", storageSection.Data["Id"]?.GetValue<string>());
    }
 
    [Fact]
    public async Task EnsureProvisioned_WaitsForReferencedAzureResources()
    {
        var builder = CreateBuilder(isRunMode: true);
        var testBicepProvisioner = new BlockingTestBicepProvisioner();
 
        AddTestAzureProvisioning(builder, bicepProvisioner: testBicepProvisioner);
 
        var storage = new AzureProvisioningResource("storage", _ => { });
        storage.Outputs["name"] = "storage";
        var storageRoles = new AzureProvisioningResource("storage-roles", infra =>
        {
            new BicepOutputReference("name", storage).AsProvisioningParameter(infra);
        });
        builder.AddResource(storageRoles);
        builder.AddResource(storage);
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        var reprovisionTask = controller.EnsureProvisionedAsync(model, CancellationToken.None);
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            reprovisionTask,
            "Provisioning completed before the first resource started provisioning.");
        Assert.Equal(["storage"], testBicepProvisioner.ProvisionedResources);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        await reprovisionTask;
 
        Assert.Equal(["storage", "storage-roles"], testBicepProvisioner.ProvisionedResources);
    }
 
    [Fact]
    public async Task EnsureProvisioned_FaultsDependentsWhenDependencyProvisioningFails()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner());
 
        var storage = new AzureProvisioningResource("storage", _ => { });
        storage.Outputs["name"] = "storage";
        var storageRoles = new AzureProvisioningResource("storage-roles", infra =>
        {
            new BicepOutputReference("name", storage).AsProvisioningParameter(infra);
        });
        builder.AddResource(storageRoles);
        builder.AddResource(storage);
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.EnsureProvisionedAsync(model);
 
        Assert.True(notifications.TryGetCurrentState(storage.Name, out var storageEvent));
        Assert.Equal("Failed to Provision", storageEvent.Snapshot.State?.Text);
 
        Assert.True(notifications.TryGetCurrentState(storageRoles.Name, out var storageRolesEvent));
        Assert.Equal("Failed to Provision", storageRolesEvent.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task EnsureProvisioned_PublishesCanceledWhenFastPathCanceledDeploymentFaults()
    {
        var builder = CreateBuilder(isRunMode: true);
        var deploymentStateManager = new TestDeploymentStateManager();
        var canceledDeploymentIds = new List<string>();
        const string subscriptionId = "12345678-1234-1234-1234-123456789012";
        const string resourceGroup = "test-rg";
        const string deploymentId = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Resources/deployments/storage";
        var testBicepProvisioner = new CanceledDeploymentThenThrowingTestBicepProvisioner(deploymentStateManager);
 
        builder.Configuration["Azure:SubscriptionId"] = subscriptionId;
        builder.Configuration["Azure:Location"] = "westus2";
        builder.Configuration["Azure:ResourceGroup"] = resourceGroup;
 
        AddTestAzureProvisioning(
            builder,
            armClientProvider: ProvisioningTestHelpers.CreateArmClientProvider(
                existingResourceIds: Array.Empty<string>(),
                deletedResourceIds: null,
                deploymentTargetResourceIds: null,
                canceledDeploymentIds: canceledDeploymentIds),
            bicepProvisioner: testBicepProvisioner,
            deploymentStateManager: deploymentStateManager);
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var preparer = new AzureResourcePreparer(
            app.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>(),
            app.Services.GetRequiredService<DistributedApplicationExecutionContext>());
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await preparer.OnBeforeStartAsync(new BeforeStartEvent(app.Services, model), CancellationToken.None);
 
        var storageSection = await deploymentStateManager.AcquireSectionAsync("Azure:Deployments:storage");
        storageSection.Data[BicepUtilities.DeploymentStateIdKey] = deploymentId;
        storageSection.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateRunning;
        await deploymentStateManager.SaveSectionAsync(storageSection);
 
        var provisioningTask = controller.EnsureProvisionedAsync(model, CancellationToken.None);
 
        await WaitForSignalBeforeOperationCompletesAsync(
            testBicepProvisioner.FirstProvisionStarted.Task,
            provisioningTask,
            "Provisioning completed before the resource started provisioning.");
 
        var cancelCommand = Assert.Single(storage.Resource.Annotations.OfType<ResourceCommandAnnotation>(), c => c.Name == AzureProvisioningController.CancelCommandName);
        var cancelResult = await cancelCommand.ExecuteCommand(new ExecuteCommandContext
        {
            Services = app.Services,
            ResourceName = storage.Resource.Name,
            CancellationToken = CancellationToken.None,
            Logger = NullLogger.Instance,
            Arguments = new InteractionInputCollection([])
        }).WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(cancelResult.Success);
        Assert.Equal([deploymentId], canceledDeploymentIds);
        await testBicepProvisioner.OperationCancellationRequested.Task.WaitAsync(s_testSynchronizationTimeout);
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out var storageEvent));
        Assert.Equal(AzureProvisioningController.CancelingState, storageEvent.Snapshot.State?.Text);
 
        testBicepProvisioner.AllowFirstProvisionToComplete.TrySetResult();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => provisioningTask.WaitAsync(s_testSynchronizationTimeout));
 
        Assert.True(notifications.TryGetCurrentState(storage.Resource.Name, out storageEvent));
        Assert.Equal(AzureProvisioningStrings.ResourceStateCanceled, storageEvent.Snapshot.State?.Text);
        Assert.Equal(KnownResourceStateStyles.Info, storageEvent.Snapshot.State?.Style);
    }
 
    [Fact]
    public async Task EnsureProvisioned_AddsFailedResourceBreadcrumbsToAzureEnvironment()
    {
        var builder = CreateBuilder(isRunMode: true);
 
        AddTestAzureProvisioning(builder, bicepProvisioner: new ThrowingTestBicepProvisioner());
 
        var storage = builder.AddBicepTemplateString("storage", "resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
        var storage2 = builder.AddBicepTemplateString("storage2", "resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = {}");
 
        using var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
        var controller = app.Services.GetRequiredService<AzureProvisioningController>();
 
        await controller.EnsureProvisionedAsync(model);
 
        var azureEnvironment = Assert.Single(model.Resources.OfType<AzureEnvironmentResource>());
        Assert.True(notifications.TryGetCurrentState(azureEnvironment.Name, out var environmentEvent));
        Assert.Equal("Failed to Provision", environmentEvent.Snapshot.State?.Text);
 
        var failedResourcesProperty = Assert.Single(environmentEvent.Snapshot.Properties, property => property.Name == "azure.provisioning.error.failed.resources");
        Assert.Equal(AzureProvisioningStrings.FailurePropertyFailedResourcesDisplayName, failedResourcesProperty.DisplayName);
        Assert.True(failedResourcesProperty.IsHighlighted);
        Assert.Equal([storage.Resource.Name, storage2.Resource.Name], Assert.IsType<string[]>(failedResourcesProperty.Value));
    }
 
    [Fact]
    public void AddAzureEnvironment_InPublishMode_CreatesStableDeploymentName()
    {
        // Arrange
        var builder = CreateBuilder(isRunMode: false);
        builder.Configuration["AppHost:ProjectNameSha256"] = "ABCDE12345";
 
        // Act
        builder.AddAzureEnvironment();
 
        // Assert
        var resource = builder.Resources.OfType<AzureEnvironmentResource>().Single();
        Assert.Equal("azure-environment", resource.Name);
    }
 
    [Fact]
    public void AddAzureEnvironment_InRunMode_CreatesDiscoverableControlResourceName()
    {
        // Arrange
        var builder = CreateBuilder(isRunMode: true);
 
        // Act
        builder.AddAzureEnvironment();
 
        // Assert
        var resource = builder.Resources.OfType<AzureEnvironmentResource>().Single();
        Assert.Equal("azure-environment", resource.Name);
    }
 
    [Fact]
    public void AzureEnvironmentResource_PreservesDefaultResourceNameValidation()
    {
        var builder = CreateBuilder(isRunMode: true);
        var location = new ParameterResource("location", _ => "westus");
        var resourceGroupName = new ParameterResource("resourceGroupName", _ => "rg");
        var principalId = new ParameterResource("principalId", _ => "principal");
        var resource = new AzureEnvironmentResource("azure_environment", location, resourceGroupName, principalId);
 
        var ex = Assert.Throws<ArgumentException>(() => builder.AddResource(resource));
        Assert.Equal("Resource name 'azure_environment' is invalid. Name must contain only ASCII letters, digits, and hyphens. (Parameter 'name')", ex.Message);
    }
 
    [Fact]
    public void AddAzureEnvironment_CreatesFallbackNameWhenAzureResourceNameExists()
    {
        // Arrange
        var builder = CreateBuilder(isRunMode: true);
        builder.AddParameter("azure-environment", "value");
 
        // Act
        builder.AddAzureEnvironment();
 
        // Assert
        var resource = builder.Resources.OfType<AzureEnvironmentResource>().Single();
        Assert.Equal("azure-environment2", resource.Name);
    }
 
    [Fact]
    public void WithLocation_ShouldSetLocationProperty()
    {
        // Arrange
        var builder = CreateBuilder(isRunMode: false);
        var resourceBuilder = builder.AddAzureEnvironment();
        var expectedLocation = builder.AddParameter("location", "eastus2");
 
        // Act
        resourceBuilder.WithLocation(expectedLocation);
 
        // Assert
        var resource = builder.Resources.OfType<AzureEnvironmentResource>().Single();
        Assert.Equal(expectedLocation.Resource, resource.Location);
    }
 
    [Fact]
    public void WithResourceGroup_ShouldSetResourceGroupNameProperty()
    {
        // Arrange
        var builder = CreateBuilder(isRunMode: false);
        var resourceBuilder = builder.AddAzureEnvironment();
        var expectedResourceGroup = builder.AddParameter("resourceGroupName", "my-resource-group");
 
        // Act
        resourceBuilder.WithResourceGroup(expectedResourceGroup);
 
        // Assert
        var resource = builder.Resources.OfType<AzureEnvironmentResource>().Single();
        Assert.Equal(expectedResourceGroup.Resource, resource.ResourceGroupName);
    }
 
    private static IDistributedApplicationBuilder CreateBuilder(bool isRunMode = false)
    {
        var operation = isRunMode ? DistributedApplicationOperation.Run : DistributedApplicationOperation.Publish;
        return TestDistributedApplicationBuilder.Create(operation);
    }
 
    /// <summary>
    /// Registers test implementations of Azure provisioning services and calls
    /// <c>AddAzureProvisioning</c>. All production service registrations use
    /// <c>TryAddSingleton</c>, so pre-registering test implementations here
    /// prevents the production versions from being added. Pass custom
    /// implementations via optional parameters; defaults are used for any
    /// parameter left <c>null</c>.
    /// </summary>
    private static void AddTestAzureProvisioning(
        IDistributedApplicationBuilder builder,
        IArmClientProvider? armClientProvider = null,
        ITokenCredentialProvider? tokenCredentialProvider = null,
        IBicepProvisioner? bicepProvisioner = null,
        IProvisioningContextProvider? provisioningContextProvider = null,
        IDeploymentStateManager? deploymentStateManager = null,
        IAzureProvisioningOptionsManager? provisioningOptionsManager = null)
    {
        var stateManager = deploymentStateManager ?? new TestDeploymentStateManager();
        builder.Services.AddSingleton<IArmClientProvider>(armClientProvider ?? ProvisioningTestHelpers.CreateArmClientProvider());
        builder.Services.AddSingleton<ITokenCredentialProvider>(tokenCredentialProvider ?? ProvisioningTestHelpers.CreateTokenCredentialProvider());
        builder.Services.AddSingleton<IBicepProvisioner>(bicepProvisioner ?? new TestBicepProvisioner());
        builder.Services.AddSingleton<IDeploymentStateManager>(stateManager);
        builder.Services.AddSingleton<IProvisioningContextProvider>(provisioningContextProvider ?? new TestProvisioningContextProvider());
        builder.Services.AddSingleton<IAzureProvisioningOptionsManager>(provisioningOptionsManager ?? new TestAzureProvisioningOptionsManager(stateManager));
        builder.AddAzureProvisioning();
    }
 
    private static InteractionInputCollection CreateArguments(params (string Name, string? Value)[] values)
    {
        return new InteractionInputCollection([.. values.Select(static value => new InteractionInput
        {
            Name = value.Name,
            InputType = InputType.Text,
            Value = value.Value
        })]);
    }
 
    private static InteractionInputCollection CloneInputs(IReadOnlyList<InteractionInput> inputs)
    {
        return new InteractionInputCollection([.. inputs.Select(static input => new InteractionInput
        {
            Name = input.Name,
            Label = input.Label,
            Description = input.Description,
            EnableDescriptionMarkdown = input.EnableDescriptionMarkdown,
            InputType = input.InputType,
            Required = input.Required,
            Options = input.Options,
            DynamicLoading = input.DynamicLoading,
            Value = input.Value,
            Placeholder = input.Placeholder,
            AllowCustomChoice = input.AllowCustomChoice,
            Disabled = input.Disabled,
            MaxLength = input.MaxLength
        })]);
    }
 
    private static Task LoadInputAsync(IServiceProvider services, InteractionInputCollection inputs, InteractionInput input)
    {
        return input.DynamicLoading!.LoadCallback(new LoadInputContext
        {
            AllInputs = inputs,
            CancellationToken = CancellationToken.None,
            Input = input,
            Services = services
        });
    }
 
    private static JsonObject AssertCommandJsonData(ExecuteCommandResult result)
    {
        Assert.NotNull(result.Data);
        var data = result.Data!;
        Assert.Equal(CommandResultFormat.Json, data.Format);
        return Assert.IsType<JsonObject>(JsonNode.Parse(data.Value));
    }
 
    private static void AssertAffectedResourceCommandsDuringOperation(CustomResourceSnapshot snapshot)
    {
        AssertCommandState(snapshot, AzureProvisioningController.ChangeResourceLocationCommandName, ResourceCommandState.Disabled);
        AssertCommandState(snapshot, AzureProvisioningController.GetAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.DeleteAzureResourceCommandName, ResourceCommandState.Disabled);
        AssertCommandState(snapshot, AzureProvisioningController.ForgetStateCommandName, ResourceCommandState.Disabled);
        AssertCommandState(snapshot, AzureProvisioningController.ReprovisionResourceCommandName, ResourceCommandState.Disabled);
    }
 
    private static void AssertUnaffectedResourceCommandsDuringOperation(CustomResourceSnapshot snapshot)
    {
        AssertCommandState(snapshot, AzureProvisioningController.ChangeResourceLocationCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.GetAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.CancelCommandName, ResourceCommandState.Hidden);
        AssertCommandState(snapshot, AzureProvisioningController.DeleteAzureResourceCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.ForgetStateCommandName, ResourceCommandState.Enabled);
        AssertCommandState(snapshot, AzureProvisioningController.ReprovisionResourceCommandName, ResourceCommandState.Enabled);
    }
 
    private static void AssertCommandState(CustomResourceSnapshot snapshot, string commandName, ResourceCommandState expectedState)
    {
        var command = Assert.Single(snapshot.Commands, c => c.Name == commandName);
        Assert.Equal(expectedState, command.State);
    }
 
    private static void AssertHighlightedContextProperty(IEnumerable<ResourcePropertySnapshot> properties, string name, object value, string displayName)
    {
        var property = Assert.Single(properties, p => p.Name == name);
 
        Assert.Equal(value, property.Value);
        Assert.Equal(displayName, property.DisplayName);
        Assert.True(property.IsHighlighted);
    }
 
    private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return utcNow;
        }
    }
 
    private sealed class AzureProvisioningFailureTestResponse : Response
    {
        private readonly int _status;
        private readonly string _reasonPhrase;
 
        public AzureProvisioningFailureTestResponse(int status, string reasonPhrase, string content)
        {
            _status = status;
            _reasonPhrase = reasonPhrase;
            ContentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content));
        }
 
        public override int Status => _status;
 
        public override string ReasonPhrase => _reasonPhrase;
 
        public override Stream? ContentStream { get; set; }
 
        public override string ClientRequestId { get; set; } = string.Empty;
 
        public override void Dispose()
        {
        }
 
        protected override bool TryGetHeader(
            string name,
            [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? value)
        {
            value = null;
 
            return false;
        }
 
        protected override bool TryGetHeaderValues(
            string name,
            [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IEnumerable<string>? values)
        {
            values = null;
 
            return false;
        }
 
        protected override bool ContainsHeader(string name) => false;
 
        protected override IEnumerable<HttpHeader> EnumerateHeaders() => [];
    }
 
    private static readonly TimeSpan s_testSynchronizationTimeout = TimeSpan.FromSeconds(30);
 
    private static async Task WaitForSignalBeforeOperationCompletesAsync(Task signalTask, Task operationTask, string completionMessage)
    {
        using var watchdog = new CancellationTokenSource(s_testSynchronizationTimeout);
        Task completedTask;
 
        try
        {
            completedTask = await Task.WhenAny(signalTask, operationTask).WaitAsync(watchdog.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (watchdog.IsCancellationRequested)
        {
            Assert.Fail($"Timed out after {s_testSynchronizationTimeout} waiting for the test synchronization signal. Signal status: {signalTask.Status}. Operation status: {operationTask.Status}. {completionMessage}");
            return;
        }
 
        if (completedTask == signalTask || signalTask.IsCompleted)
        {
            await signalTask.ConfigureAwait(false);
            return;
        }
 
        if (operationTask.IsFaulted || operationTask.IsCanceled)
        {
            await operationTask.ConfigureAwait(false);
        }
 
        Assert.Fail(completionMessage);
    }
 
    private static async Task WaitForResourceStateAsync(ResourceNotificationService notifications, string resourceName, string expectedState)
    {
        using var watchdog = new CancellationTokenSource(s_testSynchronizationTimeout);
        while (!watchdog.IsCancellationRequested)
        {
            if (notifications.TryGetCurrentState(resourceName, out var resourceEvent) &&
                resourceEvent.Snapshot.State?.Text == expectedState)
            {
                return;
            }
 
            try
            {
                await Task.Delay(TimeSpan.FromMilliseconds(20), watchdog.Token).ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (watchdog.IsCancellationRequested)
            {
                break;
            }
        }
 
        var actualState = notifications.TryGetCurrentState(resourceName, out var finalEvent)
            ? finalEvent.Snapshot.State?.Text
            : "<missing>";
        Assert.Fail($"Timed out after {s_testSynchronizationTimeout} waiting for resource '{resourceName}' to reach state '{expectedState}'. Actual state: '{actualState}'.");
    }
 
    private static async Task<(IReadOnlyList<PipelineStep> Steps, PipelineContext PipelineContext)> CreateAzureEnvironmentPipelineStepsAsync(
        AzureEnvironmentResource environmentResource,
        DistributedApplicationModel model,
        IServiceProvider services)
    {
        var pipelineContext = new PipelineContext(
            model,
            services.GetRequiredService<DistributedApplicationExecutionContext>(),
            services,
            NullLogger.Instance,
            CancellationToken.None);
 
        var annotation = Assert.Single(environmentResource.Annotations.OfType<PipelineStepAnnotation>());
        var steps = await annotation.CreateStepsAsync(new PipelineStepFactoryContext
        {
            PipelineContext = pipelineContext,
            Resource = environmentResource
        });
 
        return ([.. steps], pipelineContext);
    }
 
    private sealed class AzureContextOptionsArmClientProvider(AzureContextOptionsArmClient armClient) : IArmClientProvider
    {
        public IArmClient GetArmClient(TokenCredential credential, string subscriptionId)
            => armClient;
 
        public IArmClient GetArmClient(TokenCredential credential)
            => armClient;
    }
 
    private sealed class AzureContextOptionsArmClient : IArmClient
    {
        public const string FirstTenantId = "11111111-1111-1111-1111-111111111111";
        public const string SecondTenantId = "22222222-2222-2222-2222-222222222222";
        public const string FirstSubscriptionId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
        public const string SecondSubscriptionId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
 
        private readonly IReadOnlyList<ContextTenantResource> _tenants =
        [
            new(FirstTenantId, "First tenant"),
            new(SecondTenantId, "Second tenant")
        ];
        private readonly Dictionary<string, IReadOnlyList<ContextSubscriptionResource>> _subscriptionsByTenant = new(StringComparer.OrdinalIgnoreCase)
        {
            [FirstTenantId] = [new(FirstSubscriptionId, "First subscription", FirstTenantId)],
            [SecondTenantId] = [new(SecondSubscriptionId, "Second subscription", SecondTenantId)]
        };
        private readonly Dictionary<string, IReadOnlyList<(string Name, string Location)>> _resourceGroupsBySubscription = new(StringComparer.OrdinalIgnoreCase)
        {
            [FirstSubscriptionId] = [("rg-first", "eastus")],
            [SecondSubscriptionId] = [("rg-second", "westus2")]
        };
        private readonly IReadOnlyList<(string Name, string DisplayName)> _locations =
        [
            ("eastus", "East US"),
            ("westus2", "West US 2")
        ];
 
        public string? LastSubscriptionTenantId { get; private set; }
 
        public string? LastResourceGroupSubscriptionId { get; private set; }
 
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
        {
            var subscription = _subscriptionsByTenant[FirstTenantId].Single();
            var tenant = _tenants.Single(t => t.TenantId == Guid.Parse(FirstTenantId));
            return Task.FromResult<(ISubscriptionResource, ITenantResource)>((subscription, tenant));
        }
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
        {
            IEnumerable<ITenantResource> result = _tenants;
            return Task.FromResult(result);
        }
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
        {
            IEnumerable<ISubscriptionResource> result = _subscriptionsByTenant.Values.SelectMany(static subscriptions => subscriptions);
            return Task.FromResult(result);
        }
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
        {
            LastSubscriptionTenantId = tenantId;
 
            IEnumerable<ISubscriptionResource> result = tenantId is not null && _subscriptionsByTenant.TryGetValue(tenantId, out var subscriptions)
                ? subscriptions
                : [];
            return Task.FromResult(result);
        }
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
        {
            var subscription = _subscriptionsByTenant.Values
                .SelectMany(static subscriptions => subscriptions)
                .Single(s => string.Equals(s.Id.Name, subscriptionId, StringComparison.OrdinalIgnoreCase));
            return Task.FromResult<ISubscriptionResource>(subscription);
        }
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
        {
            return Task.FromResult<IEnumerable<(string Name, string DisplayName)>>(_locations);
        }
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
        {
            LastResourceGroupSubscriptionId = subscriptionId;
 
            IEnumerable<(string Name, string Location)> result = _resourceGroupsBySubscription.TryGetValue(subscriptionId, out var resourceGroups)
                ? resourceGroups
                : [];
            return Task.FromResult(result);
        }
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => Task.FromResult<IEnumerable<string>>([]);
 
        public IRoleAssignmentCollection GetRoleAssignments(ResourceIdentifier scope)
            => throw new NotSupportedException();
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public async IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
 
        public async IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(
            string deploymentId,
            bool recursive = true,
            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class ContextTenantResource(string tenantId, string displayName) : ITenantResource
    {
        public Guid? TenantId { get; } = Guid.Parse(tenantId);
 
        public string? DisplayName { get; } = displayName;
 
        public string? DefaultDomain { get; } = $"{displayName.Replace(" ", "", StringComparison.Ordinal).ToLowerInvariant()}.onmicrosoft.com";
 
        public IArmDeploymentCollection GetArmDeployments()
            => new TestArmDeploymentCollection([]);
    }
 
    private sealed class ContextSubscriptionResource(string subscriptionId, string displayName, string tenantId) : ISubscriptionResource
    {
        public ResourceIdentifier Id { get; } = new($"/subscriptions/{subscriptionId}");
 
        public string? DisplayName { get; } = displayName;
 
        public Guid? TenantId { get; } = Guid.Parse(tenantId);
 
        public IArmDeploymentCollection GetArmDeployments()
            => new TestArmDeploymentCollection([]);
 
        public IResourceGroupCollection GetResourceGroups()
            => new TestResourceGroupCollection([]);
    }
 
    private sealed class AnnotatedAzureResource(string name) : Resource(name);
 
    private sealed class TestDeploymentStateManager : IDeploymentStateManager
    {
        private readonly object _lock = new();
        private readonly Dictionary<string, JsonObject> _sections = new(StringComparer.Ordinal);
 
        public string? StateFilePath => null;
 
        public string? SaveConflictSectionName { get; set; }
 
        public int SaveConflictCount { get; set; }
 
        public int SaveConflictThrowCount { get; private set; }
 
        public Task<DeploymentStateSection> AcquireSectionAsync(string sectionName, CancellationToken cancellationToken = default)
        {
            JsonObject data;
            lock (_lock)
            {
                _sections.TryGetValue(sectionName, out var existingData);
                data = existingData?.DeepClone().AsObject() ?? [];
            }
 
            return Task.FromResult(new DeploymentStateSection(sectionName, data, version: 0));
        }
 
        public Task<DeploymentStateSection> AcquireCurrentSectionAsync(string sectionName, CancellationToken cancellationToken = default)
            => AcquireSectionAsync(sectionName, cancellationToken);
 
        public Task DeleteSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            lock (_lock)
            {
                _sections.Remove(section.SectionName);
            }
 
            return Task.CompletedTask;
        }
 
        public Task SaveSectionAsync(DeploymentStateSection section, CancellationToken cancellationToken = default)
        {
            lock (_lock)
            {
                if (section.SectionName == SaveConflictSectionName && SaveConflictCount > 0)
                {
                    SaveConflictCount--;
                    SaveConflictThrowCount++;
                    throw new InvalidOperationException($"Concurrency conflict detected in section '{section.SectionName}'. Expected version 1, but current version is 2.");
                }
 
                _sections[section.SectionName] = section.Data.DeepClone().AsObject();
            }
 
            return Task.CompletedTask;
        }
 
        public Task ClearAllStateAsync(CancellationToken cancellationToken = default)
        {
            lock (_lock)
            {
                _sections.Clear();
            }
 
            return Task.CompletedTask;
        }
    }
 
    private sealed class TestBicepProvisioner : IBicepProvisioner
    {
        private readonly object _lock = new();
        private readonly List<string> _configuredResources = [];
        private readonly List<string> _provisionedResources = [];
        private readonly Dictionary<string, string?> _provisionedLocations = new(StringComparer.Ordinal);
 
        public int ConfigureResourceCallCount { get; private set; }
 
        public int GetOrCreateResourceCallCount { get; private set; }
 
        public IReadOnlyList<string> ConfiguredResources
        {
            get
            {
                lock (_lock)
                {
                    return [.. _configuredResources];
                }
            }
        }
 
        public IReadOnlyList<string> ProvisionedResources
        {
            get
            {
                lock (_lock)
                {
                    return [.. _provisionedResources];
                }
            }
        }
 
        public IReadOnlyDictionary<string, string?> ProvisionedLocations
        {
            get
            {
                lock (_lock)
                {
                    return new Dictionary<string, string?>(_provisionedLocations, StringComparer.Ordinal);
                }
            }
        }
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken)
        {
            lock (_lock)
            {
                ConfigureResourceCallCount++;
                _configuredResources.Add(resource.Name);
            }
 
            return Task.FromResult(false);
        }
 
        public Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            lock (_lock)
            {
                GetOrCreateResourceCallCount++;
                _provisionedResources.Add(resource.Name);
                _provisionedLocations[resource.Name] = resource.Parameters.TryGetValue(AzureBicepResource.KnownParameters.Location, out var location)
                    ? location?.ToString()
                    : null;
            }
 
            resource.Outputs["blobEndpoint"] = "https://storage.blob.core.windows.net/";
            return Task.CompletedTask;
        }
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
    }
 
    private sealed class CachedStateTestBicepProvisioner : IBicepProvisioner
    {
        public int ConfigureResourceCallCount { get; private set; }
 
        public int GetOrCreateResourceCallCount { get; private set; }
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken)
        {
            ConfigureResourceCallCount++;
            resource.Outputs["blobEndpoint"] = "https://storage.blob.core.windows.net/";
            return Task.FromResult(true);
        }
 
        public Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            GetOrCreateResourceCallCount++;
            return Task.CompletedTask;
        }
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
    }
 
    private sealed class TestProvisioningContextProvider : IProvisioningContextProvider
    {
        private readonly ProvisioningContext _context;
 
        public TestProvisioningContextProvider()
            : this(ProvisioningTestHelpers.CreateTestProvisioningContext())
        {
        }
 
        public TestProvisioningContextProvider(ProvisioningContext context)
        {
            _context = context;
        }
 
        public int CreateProvisioningContextCallCount { get; private set; }
 
        public Task<ProvisioningContext> CreateProvisioningContextAsync(CancellationToken cancellationToken = default)
        {
            CreateProvisioningContextCallCount++;
            return Task.FromResult(_context);
        }
    }
 
    private sealed class TestAzureProvisioningOptionsManager(IDeploymentStateManager deploymentStateManager) : IAzureProvisioningOptionsManager
    {
        public Task<bool> EnsureProvisioningOptionsAsync(bool forcePrompt, CancellationToken cancellationToken = default) => Task.FromResult(true);
 
        public async Task<AzureProvisioningOptionsState> GetProvisioningOptionsAsync(CancellationToken cancellationToken = default)
        {
            var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure", cancellationToken);
            return new AzureProvisioningOptionsState(
                azureSection.Data["SubscriptionId"]?.GetValue<string>(),
                azureSection.Data["ResourceGroup"]?.GetValue<string>(),
                azureSection.Data["Location"]?.GetValue<string>(),
                azureSection.Data["TenantId"]?.GetValue<string>());
        }
 
        public Task PersistProvisioningOptionsAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
 
        public async Task<AzureProvisioningOptionsState> ApplyProvisioningOptionsAsync(AzureProvisioningOptionsUpdate options, CancellationToken cancellationToken = default)
        {
            var azureSection = await deploymentStateManager.AcquireSectionAsync("Azure", cancellationToken);
            azureSection.Data["SubscriptionId"] = options.SubscriptionId;
            azureSection.Data["Location"] = options.Location;
            azureSection.Data["ResourceGroup"] = options.ResourceGroup;
 
            if (!string.IsNullOrEmpty(options.TenantId))
            {
                azureSection.Data["TenantId"] = options.TenantId;
            }
            else
            {
                azureSection.Data.Remove("TenantId");
            }
 
            await deploymentStateManager.SaveSectionAsync(azureSection, cancellationToken);
            return new AzureProvisioningOptionsState(options.SubscriptionId, options.ResourceGroup, options.Location, options.TenantId);
        }
    }
 
    private sealed class BlockingTestBicepProvisioner : IBicepProvisioner
    {
        private readonly object _lock = new();
        private readonly List<string> _provisionedResources = [];
 
        public TaskCompletionSource FirstProvisionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource AllowFirstProvisionToComplete { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public IReadOnlyList<string> ProvisionedResources
        {
            get
            {
                lock (_lock)
                {
                    return [.. _provisionedResources];
                }
            }
        }
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public async Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            bool isFirstProvision;
            lock (_lock)
            {
                _provisionedResources.Add(resource.Name);
                isFirstProvision = _provisionedResources.Count == 1;
            }
 
            if (isFirstProvision)
            {
                FirstProvisionStarted.TrySetResult();
                await AllowFirstProvisionToComplete.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
            }
 
            resource.Outputs["blobEndpoint"] = $"https://{resource.Name}.blob.core.windows.net/";
        }
    }
 
    private sealed class CancellationIgnoringBlockingTestBicepProvisioner : IBicepProvisioner
    {
        public TaskCompletionSource FirstProvisionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource OperationCancellationRequested { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource AllowFirstProvisionToComplete { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public async Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            using var registration = cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(), OperationCancellationRequested);
 
            FirstProvisionStarted.TrySetResult();
            await AllowFirstProvisionToComplete.Task.ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
        }
    }
 
    private sealed class BlockingThrowingTestBicepProvisioner : IBicepProvisioner
    {
        public TaskCompletionSource FirstProvisionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource AllowFirstProvisionToThrow { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public async Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            FirstProvisionStarted.TrySetResult();
            await AllowFirstProvisionToThrow.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
            throw new InvalidOperationException("boom");
        }
    }
 
    private sealed class ThrowingTestBicepProvisioner(Exception? exception = null) : IBicepProvisioner
    {
        private readonly Exception _exception = exception ?? new InvalidOperationException("boom");
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromException(_exception);
    }
 
    private sealed class CanceledDeploymentThenThrowingTestBicepProvisioner(IDeploymentStateManager deploymentStateManager) : IBicepProvisioner
    {
        public TaskCompletionSource FirstProvisionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public TaskCompletionSource OperationCancellationRequested { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public TaskCompletionSource AllowFirstProvisionToComplete { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public async Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            using var registration = cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(), OperationCancellationRequested);
 
            FirstProvisionStarted.TrySetResult();
            await AllowFirstProvisionToComplete.Task.ConfigureAwait(false);
 
            var section = await deploymentStateManager.AcquireSectionAsync($"Azure:Deployments:{resource.Name}", cancellationToken).ConfigureAwait(false);
            section.Data[BicepProvisioner.DeploymentStateProvisioningStateKey] = BicepProvisioner.DeploymentStateProvisioningStateCanceled;
            await deploymentStateManager.SaveSectionAsync(section, cancellationToken).ConfigureAwait(false);
 
            throw new InvalidOperationException("Deployment operation completed as canceled.");
        }
    }
 
    private sealed class KeyVaultSoftDeleteConflictThenSuccessProvisioner(string keyVaultResourceId) : IBicepProvisioner
    {
        private int _getOrCreateResourceCallCount;
 
        public int GetOrCreateResourceCallCount => Volatile.Read(ref _getOrCreateResourceCallCount);
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken) => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            var callCount = Interlocked.Increment(ref _getOrCreateResourceCallCount);
            if (callCount == 1)
            {
                var failure = new AzureProvisioningFailureDetails(
                    Provider: "Microsoft.KeyVault",
                    ResourceType: "Microsoft.KeyVault/vaults",
                    ResourceName: "kv-test",
                    TargetResourceId: keyVaultResourceId,
                    CurrentLocation: null,
                    SupportedLocations: [],
                    HttpStatus: 409,
                    ErrorCode: "ConflictError",
                    ErrorMessage: "A vault with the same name already exists in deleted state. You need to either recover or purge existing key vault.",
                    Operation: "Create",
                    RequestId: null,
                    CorrelationId: null,
                    RecommendedActions: []);
                return Task.FromException(new AzureProvisioningFailureException(failure, new InvalidOperationException("Key Vault is soft-deleted.")));
            }
 
            resource.Outputs["id"] = keyVaultResourceId;
            return Task.CompletedTask;
        }
    }
 
    private sealed class ParentChildOrderingBicepProvisioner(string childResourceName, string parentResourceName) : IBicepProvisioner
    {
        private readonly object _lock = new();
        private readonly List<string> _provisionedResources = [];
        private int _childCompleted;
        private int _parentStartedBeforeChildCompleted;
 
        public TaskCompletionSource ChildProvisionStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public TaskCompletionSource AllowChildProvisionToComplete { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
 
        public bool ParentStartedBeforeChildCompleted => Volatile.Read(ref _parentStartedBeforeChildCompleted) == 1;
 
        public IReadOnlyList<string> ProvisionedResources
        {
            get
            {
                lock (_lock)
                {
                    return [.. _provisionedResources];
                }
            }
        }
 
        public Task<bool> ConfigureResourceAsync(AzureBicepResource resource, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public Task<bool> ReconcileDeploymentStateAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
            => Task.FromResult(false);
 
        public async Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningContext context, CancellationToken cancellationToken)
        {
            if (string.Equals(resource.Name, childResourceName, StringComparison.Ordinal))
            {
                AddProvisionedResource(resource);
                ChildProvisionStarted.TrySetResult();
                await AllowChildProvisionToComplete.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
                Volatile.Write(ref _childCompleted, 1);
                return;
            }
 
            if (string.Equals(resource.Name, parentResourceName, StringComparison.Ordinal) &&
                Volatile.Read(ref _childCompleted) == 0)
            {
                Volatile.Write(ref _parentStartedBeforeChildCompleted, 1);
            }
 
            AddProvisionedResource(resource);
        }
 
        private void AddProvisionedResource(AzureBicepResource resource)
        {
            lock (_lock)
            {
                _provisionedResources.Add(resource.Name);
            }
 
            resource.Outputs["id"] = $"/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Test/resources/{resource.Name}";
            resource.Outputs["name"] = resource.Name;
            resource.Outputs["vaultUri"] = $"https://{resource.Name}.vault.azure.net/";
            resource.Outputs["connectionString"] = $"Host={resource.Name}.postgres.database.azure.com";
            resource.Outputs["hostName"] = $"{resource.Name}.postgres.database.azure.com";
        }
    }
 
    private sealed class CancelConflictArmClientProvider : IArmClientProvider
    {
        public IArmClient GetArmClient(global::Azure.Core.TokenCredential credential, string subscriptionId)
            => new CancelConflictArmClient();
 
        public IArmClient GetArmClient(global::Azure.Core.TokenCredential credential)
            => new CancelConflictArmClient();
    }
 
    private sealed class CancelConflictArmClient : IArmClient
    {
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => Task.FromResult<IEnumerable<string>>([]);
 
        public IRoleAssignmentCollection GetRoleAssignments(ResourceIdentifier scope)
            => throw new NotSupportedException();
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new RequestFailedException(409, "The deployment is already completed.");
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public async IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
 
        public async IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(
            string deploymentId,
            bool recursive = true,
            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class BlockingDeleteArmClientProvider(BlockingDeleteArmClient armClient) : IArmClientProvider
    {
        public IArmClient GetArmClient(TokenCredential credential, string subscriptionId)
            => armClient;
 
        public IArmClient GetArmClient(TokenCredential credential)
            => armClient;
    }
 
    private sealed class BlockingDeleteArmClient : IArmClient
    {
        private readonly object _lock = new();
        private readonly List<string> _deletedResourceIds = [];
 
        public TaskCompletionSource DeleteStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public TaskCompletionSource AllowDeleteToComplete { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
        public IReadOnlyList<string> DeletedResourceIds
        {
            get
            {
                lock (_lock)
                {
                    return [.. _deletedResourceIds];
                }
            }
        }
 
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => Task.FromResult<IEnumerable<string>>([]);
 
        public IRoleAssignmentCollection GetRoleAssignments(ResourceIdentifier scope)
            => throw new NotSupportedException();
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => Task.FromResult(true);
 
        public async Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
        {
            lock (_lock)
            {
                _deletedResourceIds.Add(resourceId);
            }
 
            DeleteStarted.TrySetResult();
            await AllowDeleteToComplete.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
        }
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => Task.FromResult(true);
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => Task.CompletedTask;
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public async IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
 
        public async IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(
            string deploymentId,
            bool recursive = true,
            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class CredentialUnavailableArmClientProvider : IArmClientProvider
    {
        public IArmClient GetArmClient(global::Azure.Core.TokenCredential credential, string subscriptionId)
            => new CredentialUnavailableArmClient();
 
        public IArmClient GetArmClient(global::Azure.Core.TokenCredential credential)
            => new CredentialUnavailableArmClient();
    }
 
    private sealed class CredentialUnavailableArmClient : IArmClient
    {
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => Task.FromResult<IEnumerable<string>>([]);
 
        public IRoleAssignmentCollection GetRoleAssignments(global::Azure.Core.ResourceIdentifier scope)
            => throw new NotSupportedException();
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => Task.FromException<bool>(new global::Azure.Identity.CredentialUnavailableException("Credential unavailable."));
 
        public Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new global::Azure.Identity.CredentialUnavailableException("Credential unavailable.");
 
        public IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();
 
        public async IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(
            string deploymentId,
            bool recursive = true,
            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            await Task.CompletedTask;
            yield break;
        }
    }
 
    private sealed class ThrowingResourceProbeArmClientProvider(RequestFailedException exception) : IArmClientProvider
    {
        public IArmClient GetArmClient(TokenCredential credential, string subscriptionId)
            => new ThrowingResourceProbeArmClient(exception);
 
        public IArmClient GetArmClient(TokenCredential credential)
            => new ThrowingResourceProbeArmClient(exception);
    }
 
    private sealed class ThrowingResourceProbeArmClient(RequestFailedException exception) : IArmClient
    {
        private readonly TestArmClient _inner = new();
 
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
            => _inner.GetSubscriptionAndTenantAsync(cancellationToken);
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
            => _inner.GetAvailableTenantsAsync(cancellationToken);
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
            => _inner.GetAvailableSubscriptionsAsync(cancellationToken);
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableSubscriptionsAsync(tenantId, cancellationToken);
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetSubscriptionAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableLocationsAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableResourceGroupsWithLocationAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => _inner.GetSupportedLocationsAsync(subscriptionId, resourceType, cancellationToken);
 
        public IRoleAssignmentCollection GetRoleAssignments(ResourceIdentifier scope)
            => _inner.GetRoleAssignments(scope);
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => Task.FromException<bool>(exception);
 
        public Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
            => _inner.DeleteResourceAsync(resourceId, cancellationToken);
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => _inner.PurgeDeletedKeyVaultAsync(resourceId, location, cancellationToken);
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.CancelDeploymentAsync(deploymentId, cancellationToken);
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentAsync(deploymentId, cancellationToken);
 
        public IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentTargetResourceIdsAsync(deploymentId, cancellationToken);
 
        public IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(string deploymentId, bool recursive = true, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentOperationsAsync(deploymentId, recursive, cancellationToken);
    }
 
    private sealed class DeleteResourceFailureArmClientProvider(string existingResourceId, RequestFailedException deleteException) : IArmClientProvider
    {
        public IArmClient GetArmClient(TokenCredential credential, string subscriptionId)
            => new DeleteResourceFailureArmClient(existingResourceId, deleteException);
 
        public IArmClient GetArmClient(TokenCredential credential)
            => new DeleteResourceFailureArmClient(existingResourceId, deleteException);
    }
 
    private sealed class DeleteResourceFailureArmClient(string existingResourceId, RequestFailedException deleteException) : IArmClient
    {
        private readonly TestArmClient _inner = new();
 
        public Task<(ISubscriptionResource subscription, ITenantResource tenant)> GetSubscriptionAndTenantAsync(CancellationToken cancellationToken = default)
            => _inner.GetSubscriptionAndTenantAsync(cancellationToken);
 
        public Task<IEnumerable<ITenantResource>> GetAvailableTenantsAsync(CancellationToken cancellationToken = default)
            => _inner.GetAvailableTenantsAsync(cancellationToken);
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(CancellationToken cancellationToken = default)
            => _inner.GetAvailableSubscriptionsAsync(cancellationToken);
 
        public Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsAsync(string? tenantId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableSubscriptionsAsync(tenantId, cancellationToken);
 
        public Task<ISubscriptionResource> GetSubscriptionAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetSubscriptionAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableLocationsAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<(string Name, string Location)>> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default)
            => _inner.GetAvailableResourceGroupsWithLocationAsync(subscriptionId, cancellationToken);
 
        public Task<IEnumerable<string>> GetSupportedLocationsAsync(string subscriptionId, string resourceType, CancellationToken cancellationToken = default)
            => _inner.GetSupportedLocationsAsync(subscriptionId, resourceType, cancellationToken);
 
        public IRoleAssignmentCollection GetRoleAssignments(ResourceIdentifier scope)
            => _inner.GetRoleAssignments(scope);
 
        public Task<bool> ResourceExistsAsync(string resourceId, CancellationToken cancellationToken = default)
            => Task.FromResult(string.Equals(resourceId, existingResourceId, StringComparison.OrdinalIgnoreCase));
 
        public Task DeleteResourceAsync(string resourceId, CancellationToken cancellationToken = default)
            => string.Equals(resourceId, existingResourceId, StringComparison.OrdinalIgnoreCase)
                ? Task.FromException(deleteException)
                : _inner.DeleteResourceAsync(resourceId, cancellationToken);
 
        public Task<bool> PurgeDeletedKeyVaultAsync(string resourceId, string location, CancellationToken cancellationToken = default)
            => _inner.PurgeDeletedKeyVaultAsync(resourceId, location, cancellationToken);
 
        public Task CancelDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.CancelDeploymentAsync(deploymentId, cancellationToken);
 
        public Task<AzureDeploymentState?> GetDeploymentAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentAsync(deploymentId, cancellationToken);
 
        public IAsyncEnumerable<string> GetDeploymentTargetResourceIdsAsync(string deploymentId, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentTargetResourceIdsAsync(deploymentId, cancellationToken);
 
        public IAsyncEnumerable<AzureDeploymentOperationDetails> GetDeploymentOperationsAsync(string deploymentId, bool recursive = true, CancellationToken cancellationToken = default)
            => _inner.GetDeploymentOperationsAsync(deploymentId, recursive, cancellationToken);
    }
 
}