// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Bicep.Core.Diagnostics;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests
{
    [TestClass]
    public class NullableExistingResourceTests
    {
        [TestMethod]
        public void NullableExisting_FullyQualifiedDecoratorShouldWork()
        {
            // Test that @sys.nullIfNotFound() (fully qualified) works the same as @nullIfNotFound()
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@sys.nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output resourceId string = storageAccount.id
output location string? = storageAccount.?location
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                // Verify nullIfNotFound is added to @options
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
            }
        }

        [TestMethod]
        public void NullableExisting_ResourceTypeShouldBehaveAsConditional()
        {
            // Nullable existing resources should behave like conditional resources:
            // - Direct access to runtime properties should produce a warning
            // - Access to compile-time properties (id, name, type, apiVersion) should not produce warnings
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

// Access to compile-time properties - no warning expected
output resourceType string = storageAccount.type
output resourceId string = storageAccount.id
output resourceName string = storageAccount.name
output resourceApiVersion string = storageAccount.apiVersion
");
            using (new AssertionScope())
            {
                // No warnings for compile-time properties (same as conditional resources)
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                // Template should still be generated
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                // Verify nullIfNotFound is added to @options
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
                // Verify compile-time property output expressions
                template.Should().HaveValueAtPath("$.outputs['resourceType'].value", "Microsoft.Storage/storageAccounts");
                template.Should().HaveValueAtPath("$.outputs['resourceId'].value", "[resourceId('Microsoft.Storage/storageAccounts', 'testStorage')]");
                template.Should().HaveValueAtPath("$.outputs['resourceName'].value", "testStorage");
                template.Should().HaveValueAtPath("$.outputs['resourceApiVersion'].value", "2021-04-01");
            }
        }

        [TestMethod]
        public void NullableExisting_RuntimePropertyAccess_ShouldProduceWarning()
        {
            // Access to runtime properties should produce a warning (same as conditional resources)
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output skuName string = storageAccount.sku.name
");
            using (new AssertionScope())
            {
                // Should have warning because storageAccount might not exist at deployment time
                diagnostics.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]
                {
                    ("BCP318", DiagnosticLevel.Warning, "The value of type \"Microsoft.Storage/storageAccounts | null\" may be null at the start of the deployment, which would cause this access expression (and the overall deployment with it) to fail.")
                });
                // Template should still be generated (warning, not error)
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                // Verify nullIfNotFound is added to @options
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
                // Verify the output expression for runtime property access
                template.Should().HaveValueAtPath("$.outputs['skuName'].value", "[reference('storageAccount', '2021-04-01', 'full').sku.name]");
            }
        }

        [TestMethod]
        public void NullableExisting_RegularExistingShouldStillWork()
        {
            // Regular existing (without @nullIfNotFound()) should still work the same way
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output accountId string = storageAccount.id
");
            using (new AssertionScope())
            {
                // Regular existing should still be non-nullable
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                // Regular existing should NOT have nullIfNotFound in @options
                // Verify the output expression for regular existing resource
                template.Should().HaveValueAtPath("$.outputs['accountId'].value", "[resourceId('Microsoft.Storage/storageAccounts', 'testStorage')]");
            }
        }

        [TestMethod]
        public void NullableExisting_NullComparisonShouldWork()
        {
            // Null comparison should work with nullable existing resources
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output exists bool = storageAccount != null
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                // Verify nullIfNotFound is added to @options
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
                // Verify the output expression for null comparison
                template.Should().HaveValueAtPath("$.outputs['exists'].value", "[not(equals(reference('storageAccount', '2021-04-01', 'full'), null()))]");
            }
        }

        [TestMethod]
        public void NullableExisting_DecoratorCannotBeUsedWithNewResource()
        {
            // The @nullIfNotFound() decorator should produce an error on non-existing resources
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: 'testStorage'
  location: 'westus'
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}
");
            using (new AssertionScope())
            {
                template.Should().NotHaveValue();
                diagnostics.ExcludingLinterDiagnostics().Should().ContainDiagnostic("BCP445", DiagnosticLevel.Error, "The \"@nullIfNotFound()\" decorator can only be used on existing resources.");
            }
        }

        [TestMethod]
        public void NullableExisting_PropertyAccess_WithSafeAccess_ShouldWork()
        {
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output location string? = storageAccount.?location
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                // Verify nullIfNotFound is added to @options
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
                // Verify the output expression uses tryGet for safe access to location
                template.Should().HaveValueAtPath("$.outputs['location'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'location')]");
            }
        }

        [TestMethod]
        public void NullableExisting_SafeAccessAndNullCoalescing_OnRuntimeProperties()
        {
            // Test safe access (.?) and null coalescing (??) on runtime properties that would produce warnings without them
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

// Safe access on runtime properties - these would produce BCP318 warnings without .?
output safeLocation string? = storageAccount.?location
output safeSkuName string? = storageAccount.?sku.name
output safeSkuTier string? = storageAccount.?sku.tier
output safeAccessTier string? = storageAccount.?properties.accessTier
output safePrimaryEndpoints object? = storageAccount.?properties.primaryEndpoints

// Null coalescing with safe access - provides fallback values
output locationWithDefault string = storageAccount.?location ?? 'westus'
output skuNameWithDefault string = storageAccount.?sku.name ?? 'Standard_LRS'
output accessTierWithDefault string = storageAccount.?properties.accessTier ?? 'Hot'
");
            using (new AssertionScope())
            {
                // No warnings when using safe access operators
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());

                // Verify safe access outputs use tryGet
                template.Should().HaveValueAtPath("$.outputs['safeLocation'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'location')]");
                template.Should().HaveValueAtPath("$.outputs['safeSkuName'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'sku', 'name')]");
                template.Should().HaveValueAtPath("$.outputs['safeSkuTier'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'sku', 'tier')]");
                template.Should().HaveValueAtPath("$.outputs['safeAccessTier'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'properties', 'accessTier')]");
                template.Should().HaveValueAtPath("$.outputs['safePrimaryEndpoints'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'properties', 'primaryEndpoints')]");

                // Verify null coalescing outputs use coalesce with tryGet
                template.Should().HaveValueAtPath("$.outputs['locationWithDefault'].value", "[coalesce(tryGet(reference('storageAccount', '2021-04-01', 'full'), 'location'), 'westus')]");
                template.Should().HaveValueAtPath("$.outputs['skuNameWithDefault'].value", "[coalesce(tryGet(reference('storageAccount', '2021-04-01', 'full'), 'sku', 'name'), 'Standard_LRS')]");
                template.Should().HaveValueAtPath("$.outputs['accessTierWithDefault'].value", "[coalesce(tryGet(reference('storageAccount', '2021-04-01', 'full'), 'properties', 'accessTier'), 'Hot')]");
            }
        }

        [TestMethod]
        public void NullableExisting_DependsOn_ShouldBePopulated()
        {
            // Test that dependsOn is correctly populated when a new resource references a nullable existing resource
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource existingStorage 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'existingStorageAccount'
}

resource newStorage 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: 'newStorageAccount'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: existingStorage.?properties.accessTier
  }
}
");
            using (new AssertionScope())
            {
                // Should compile without errors (warning is acceptable for the coalesce pattern)
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();

                // Verify the nullable existing resource has the correct options
                template.Should().HaveValueAtPath("$.resources['existingStorage'].existing", true);
                template.Should().HaveValueAtPath("$.resources['existingStorage']['@options'].nullIfNotFound", new JArray());

                // Verify the new resource has dependsOn referencing the nullable existing resource
                template.Should().HaveValueAtPath("$.resources['newStorage'].dependsOn[0]", "existingStorage");

                // Verify expression
                template.Should().HaveValueAtPath("$.resources['newStorage'].properties.accessTier", "[tryGet(reference('existingStorage', '2021-04-01', 'full'), 'properties', 'accessTier')]");
            }
        }

        [TestMethod]
        public void NullableExisting_ConditionalListKeys_WithNullCheck()
        {
            // Test using null check with ternary to conditionally call listKeys on a nullable existing resource
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
}

output keys object = storageAccount != null ? storageAccount!.listKeys() : {}
output firstKey string = storageAccount != null ? storageAccount!.listKeys().keys[0].value : ''
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());

                // Verify the conditional listKeys output expression
                template.Should().HaveValueAtPath("$.outputs['keys'].value", "[if(not(equals(reference('storageAccount', '2021-04-01', 'full'), null())), listKeys('storageAccount', '2021-04-01'), createObject())]");
                template.Should().HaveValueAtPath("$.outputs['firstKey'].value", "[if(not(equals(reference('storageAccount', '2021-04-01', 'full'), null())), listKeys('storageAccount', '2021-04-01').keys[0].value, '')]");
            }
        }

        [TestMethod]
        public void NullableExisting_WithForLoop_ShouldCompile()
        {
            // Test that nullable existing resources work with for loops
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
param storageNames array = ['storage1', 'storage2', 'storage3']

@nullIfNotFound()
resource storageAccounts 'Microsoft.Storage/storageAccounts@2021-04-01' existing = [for name in storageNames: {
  name: name
}]

// Access a specific item by index and use safe access for nullable type
output firstAccountId string? = storageAccounts[0].?id
output secondAccountLocation string? = storageAccounts[1].?location
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                // Verify the resource is marked as existing with nullable option
                template.Should().HaveValueAtPath("$.resources['storageAccounts'].existing", true);
                template.Should().HaveValueAtPath("$.resources['storageAccounts']['@options'].nullIfNotFound", new JArray());
                // Verify copy loop is present
                template.Should().HaveValueAtPath("$.resources['storageAccounts'].copy.name", "storageAccounts");
            }
        }

        [TestMethod]
        public void NullableExisting_AsParentForChildResource_Compiles()
        {
            // Test using a nullable existing resource (key vault) as parent for a child resource (secret)
            // Note: The parent property accepts the nullable resource and compiles successfully.
            // The deployment may fail at runtime if the key vault doesn't exist.
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
@nullIfNotFound()
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'myKeyVault'
}

resource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
  parent: keyVault
  name: 'mySecret'
  properties: {
    value: 'secretValue'
  }
}

output secretId string = secret.id
");
            using (new AssertionScope())
            {
                // The parent property accepts the nullable resource without warning
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                // Verify the nullable existing key vault
                template.Should().HaveValueAtPath("$.resources['keyVault'].existing", true);
                template.Should().HaveValueAtPath("$.resources['keyVault']['@options'].nullIfNotFound", new JArray());
                // Verify the secret references the key vault as parent
                template.Should().HaveValueAtPath("$.resources['secret'].name", "[format('{0}/{1}', 'myKeyVault', 'mySecret')]");
            }
        }

        [TestMethod]
        public void NullableExisting_WithScope_ShouldCompile()
        {
            // Test that nullable existing resources work with scope property
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
param subscriptionId string
param resourceGroupName string

resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' existing = {
  name: resourceGroupName
  scope: subscription(subscriptionId)
}

@nullIfNotFound()
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: 'testStorage'
  scope: rg
}

output accountId string = storageAccount.id
output accountLocation string? = storageAccount.?location
");
            using (new AssertionScope())
            {
                // Should compile without warnings
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                // Verify the nullable existing resource has correct options
                template.Should().HaveValueAtPath("$.resources['storageAccount'].existing", true);
                template.Should().HaveValueAtPath("$.resources['storageAccount']['@options'].nullIfNotFound", new JArray());
                // Verify subscriptionId and resourceGroup are correctly set (not a scope property)
                template.Should().HaveValueAtPath("$.resources['storageAccount'].subscriptionId", "[parameters('subscriptionId')]");
                template.Should().HaveValueAtPath("$.resources['storageAccount'].resourceGroup", "[parameters('resourceGroupName')]");
                // Verify output expressions (note the leading / in the format string for extensionResourceId)
                template.Should().HaveValueAtPath("$.outputs['accountId'].value", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('subscriptionId'), parameters('resourceGroupName')), 'Microsoft.Storage/storageAccounts', 'testStorage')]");
                template.Should().HaveValueAtPath("$.outputs['accountLocation'].value", "[tryGet(reference('storageAccount', '2021-04-01', 'full'), 'location')]");
            }
        }

        [TestMethod]
        public void NullableExisting_NestedChildResource_ShouldEmitExperimentalLanguageVersion()
        {
            // @nullIfNotFound on a nested existing child resource should still trigger the experimental language version
            var (template, diagnostics, _) = CompilationHelper.Compile(@"
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: 'testStorage'
  location: 'westus'
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'

  @nullIfNotFound()
  resource blobService 'blobServices' existing = {
    name: 'default'
  }
}

output blobServiceId string? = storageAccount::blobService.?id
");
            using (new AssertionScope())
            {
                diagnostics.ExcludingLinterDiagnostics().Should().BeEmpty();
                template.Should().NotBeNull();
                // Verify the experimental language version is used (required for @nullIfNotFound)
                template.Should().HaveValueAtPath("$.languageVersion", "2.1-experimental");
                // Verify the nested existing resource has the correct options
                template.Should().HaveValueAtPath("$.resources['storageAccount::blobService'].existing", true);
                template.Should().HaveValueAtPath("$.resources['storageAccount::blobService']['@options'].nullIfNotFound", new JArray());
            }
        }
    }
}
