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

using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Bicep.Core.Diagnostics;
using Bicep.Core.UnitTests;
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 ParameterFileTests
{
    [NotNull]
    public TestContext? TestContext { get; set; }

    [TestMethod]
    public void Parameters_file_cannot_reference_non_bicep_files()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using './foo.bicepparam'
"),
("foo.bicepparam", ""));

        result.Should().OnlyContainDiagnostic("BCP276", DiagnosticLevel.Error, "A using declaration can only reference a Bicep file.");
    }

    [TestMethod]
    public void Parameters_file_cannot_self_reference()
    {
        var result = CompilationHelper.CompileParams(("parameters.bicepparam", @"
using './parameters.bicepparam'
"));

        result.Should().OnlyContainDiagnostic("BCP278", DiagnosticLevel.Error, "This parameters file references itself, which is not allowed.");
    }

    [TestMethod]
    public void Parameters_file_cycles_should_be_detected()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using './one.bicepparam'
"),
("one.bicepparam", @"
using './two.bicepparam'
"),
("two.bicepparam", @"
using './one.bicepparam'
"));

        result.Diagnostics.Should().SatisfyRespectively(
            x =>
            {
                x.Code.Should().Be("BCP095");
                x.Level.Should().Be(DiagnosticLevel.Error);
                x.Message.Should().StartWith("The file is involved in a cycle (\"");
                x.Message.Should().EndWith("\").");
                x.Message.Should().ContainAll("one.bicepparam\" -> \"", "two.bicepparam\").");
            });
    }

    [TestMethod]
    public void Params_file_with_not_using_declaration_should_log_diagnostic()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
    param foo = 'bar'
    "),
("main.bicep", @"
    param foo string
    "));

        using (new AssertionScope())
        {
            result.Parameters.Should().BeNull();
            result.Diagnostics.Should().HaveDiagnostics(new[]
            {
                    ("BCP261", DiagnosticLevel.Error, "A using declaration must be present in this parameters file.")
                });
        }
    }

    [TestMethod]
    public void Parameters_file_cannot_reference_non_existing_env_variable()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using 'foo.bicep'
param fromEnv=readEnvironmentVariable('stringEnvVariable')
"),
("foo.bicep", @"param fromEnv string"));

        result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[]{
            ("BCP427", DiagnosticLevel.Error,
                "Environment variable \"stringEnvVariable\" does not exist and there's no default value set.")
        });
    }

    [TestMethod]
    public void Parameters_file_cannot_reference_non_existing_env_variable_verbose()
    {
        var result = CompilationHelper.CompileParams(
("bicepconfig.json", """
{
  "analyzers": {
    "core": {
      "rules": {
      },
      "verbose": true
    }
  }
}
"""),
("parameters.bicepparam", @"
using 'foo.bicep'
param fromEnv=readEnvironmentVariable('stringEnvVariable')
"),
("foo.bicep", @"param fromEnv string"));

        result.Should().ContainDiagnostic("BCP427", DiagnosticLevel.Error, "Environment variable \"stringEnvVariable\" does not exist and there's no default value set.");
        result.Should().ContainDiagnostic("Bicepparam ReadEnvironmentVariable function", DiagnosticLevel.Info, "Available environment variables are: ");
    }
    [TestMethod]
    public void Parameters_file_can_use_variables()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using 'foo.bicep'

var abc = 'abc'
var abcWrapped = '>>${foo}<<'

param foo = abc
param bar = {
  abc: abc
  abcWrapped: abcWrapped
  abcUpper: toUpper(abc)
}

var list = 'FOO,BAR,BAZ'
param baz = join(map(range(0, 3), i => split(list, ',')[2 - i]), ',')
"),
("foo.bicep", @"
param foo string
param bar object
param baz string
"));

        result.Diagnostics.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);

        parameters["foo"].Value.Should().DeepEqual("abc");
        parameters["bar"].Value.Should().DeepEqual(new JObject
        {
            ["abc"] = "abc",
            ["abcWrapped"] = ">>abc<<",
            ["abcUpper"] = "ABC",
        });
        parameters["baz"].Value.Should().DeepEqual("BAZ,BAR,FOO");
    }

    [TestMethod]
    public void Parameters_file_compilation_is_blocked_on_variable_errors()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using 'foo.bicep'

var abc = utcNow()
param foo = abc
"),
("foo.bicep", @"
param foo string
"));

        result.Should().NotGenerateParameters();
        result.Should().HaveDiagnostics(new[] {
            ("BCP065", DiagnosticLevel.Error, """Function "utcNow" is not valid at this location. It can only be used as a parameter default value."""),
            ("BCP062", DiagnosticLevel.Error, """The referenced declaration with name "abc" is not valid."""),
        });
    }

    [TestMethod]
    public void Parameters_file_permits_omission_of_optional_params()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using 'foo.bicep'

param foo = 'foo'
"),
("foo.bicep", @"
param foo string
param optionalBecauseNullable string?
param optionalBecauseDefault string = 'default'
"));

        result.Should().NotHaveAnyDiagnostics();
    }

    [TestMethod]
    public void Parameters_file_permits_nulling_out_of_optional_params()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using 'foo.bicep'

param foo = 'foo'
param optionalBecauseNullable = null
param optionalBecauseDefault = null
"),
("foo.bicep", @"
param foo string
param optionalBecauseNullable string?
param optionalBecauseDefault string = 'default'
"));

        result.Should().NotHaveAnyDiagnostics();
    }

    [TestMethod]
    public void Error_is_displayed_for_file_reference_with_errors()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", """
using 'main.bicep'
"""), ("main.bicep", """
invalid file
"""));

        result.Should().HaveDiagnostics(new[]
        {
            ("BCP104", DiagnosticLevel.Error, "The referenced module has errors."),
        });
    }

    [TestMethod]
    public void GetSecret_without_expressions_in_parameters_generates_expected_keyVault_reference()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", """
using none
param foo = getSecret('subId', 'rgName', 'kvName', 'secretName', 'secretVersion')
param bar = getSecret('subId', 'rgName', 'kvName', 'secretName')
"""));
        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Reference.Should().NotBeNull();
        parameters["foo"].Reference.Should().DeepEqual(new JObject
        {
            ["keyVault"] = new JObject
            {
                ["id"] = "/subscriptions/subId/resourceGroups/rgName/providers/Microsoft.KeyVault/vaults/kvName",
            },
            ["secretName"] = "secretName",
            ["secretVersion"] = "secretVersion",
        });

        parameters["bar"].Reference.Should().NotBeNull();
        parameters["bar"].Reference.Should().DeepEqual(new JObject
        {
            ["keyVault"] = new JObject
            {
                ["id"] = "/subscriptions/subId/resourceGroups/rgName/providers/Microsoft.KeyVault/vaults/kvName",
            },
            ["secretName"] = "secretName"
        });
    }

    [TestMethod]
    public void GetSecret_expressions_in_parameters_generates_expected_keyVault_reference()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
using none

param foo = getSecret(
  externalInput('subId'),
  externalInput('rgName'),
  externalInput('kvName'),
  externalInput('secretName'),
  externalInput('secretVersion'))

var subId = externalInput('subId')
var rgName = externalInput('rgName')
var kvName = externalInput('kvName')
var secretName = externalInput('secretName')

param bar = getSecret(subId, rgName, kvName, secretName)
"""));
        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Reference.Should().NotBeNull();
        parameters["foo"].Reference.Should().DeepEqual(new JObject
        {
            ["keyVault"] = new JObject
            {
                ["id"] = "[resourceId(externalInputs('subId_0'), externalInputs('rgName_1'), 'Microsoft.KeyVault', 'vaults', externalInputs('kvName_2'))]"
            },
            ["secretName"] = "[externalInputs('secretName_3')]",
            ["secretVersion"] = "[externalInputs('secretVersion_4')]"
        });
        parameters["bar"].Reference.Should().NotBeNull();
        parameters["bar"].Reference.Should().DeepEqual(new JObject
        {
            ["keyVault"] = new JObject
            {
                ["id"] = "[resourceId(externalInputs('subId_0'), externalInputs('rgName_1'), 'Microsoft.KeyVault', 'vaults', externalInputs('kvName_2'))]"
            },
            ["secretName"] = "[externalInputs('secretName_3')]"
        });

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["subId_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "subId",
        });
        externalInputs["rgName_1"].Should().DeepEqual(new JObject
        {
            ["kind"] = "rgName",
        });
        externalInputs["kvName_2"].Should().DeepEqual(new JObject
        {
            ["kind"] = "kvName",
        });
        externalInputs["secretName_3"].Should().DeepEqual(new JObject
        {
            ["kind"] = "secretName",
        });
        externalInputs["secretVersion_4"].Should().DeepEqual(new JObject
        {
            ["kind"] = "secretVersion",
        });
    }

    [TestMethod]
    public void All_getSecret_forms_emit_keyVault_references()
    {
        var parametersResult = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
using none

param fromGetSecret = getSecret('subId', 'rgName', 'kvName', 'secretName', 'secretVersion')
param fromAzGetSecret = az.getSecret('subId', 'rgName', 'kvName', 'secretName', 'secretVersion')
param fromGetSecretWithoutVersion = getSecret('subId', 'rgName', 'kvName', 'secretName')
param fromAzGetSecretWithoutVersion = az.getSecret('subId', 'rgName', 'kvName', 'secretName')
"""));

        var templateResult = CompilationHelper.Compile(
            ("main.bicep", """
resource kv 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
  name: 'kvName'
}

module secret 'secret.bicep' = {
  name: 'secret'
  params: {
    fromResourceGetSecret: kv.getSecret('secretName', 'secretVersion')
    fromResourceGetSecretWithoutVersion: kv.getSecret('secretName')
  }
}
"""),
            ("secret.bicep", """
@secure()
param fromResourceGetSecret string

@secure()
param fromResourceGetSecretWithoutVersion string
"""));

        using (new AssertionScope())
        {
            parametersResult.Should().NotHaveAnyDiagnostics();
            templateResult.Should().NotHaveAnyDiagnostics();
        }

        var parameters = TemplateHelper.ConvertAndAssertParameters(parametersResult.Parameters);

        AssertGetSecretForms(parameters, templateResult.Template, "fromGetSecret", "fromAzGetSecret", "fromResourceGetSecret", "secretVersion");
        AssertGetSecretForms(parameters, templateResult.Template, "fromGetSecretWithoutVersion", "fromAzGetSecretWithoutVersion", "fromResourceGetSecretWithoutVersion", null);
    }

    private static void AssertGetSecretForms(
        ImmutableDictionary<string, TemplateHelper.ParameterProperties> parameters,
        JToken? template,
        string getSecretParameterName,
        string azGetSecretParameterName,
        string resourceGetSecretParameterName,
        string? expectedSecretVersion)
    {
        var getSecretReference = parameters[getSecretParameterName].Reference;
        var azGetSecretReference = parameters[azGetSecretParameterName].Reference;
        var resourceGetSecretParameter = template!.SelectToken($"$.resources[?(@.name == 'secret')].properties.parameters.{resourceGetSecretParameterName}")!;
        var resourceGetSecretReference = resourceGetSecretParameter.SelectToken("$.reference");

        using (new AssertionScope())
        {
            parameters[getSecretParameterName].Value.Should().BeNull();
            parameters[azGetSecretParameterName].Value.Should().BeNull();
            resourceGetSecretParameter.SelectToken("$.value").Should().BeNull();

            getSecretReference.Should().NotBeNull();
            azGetSecretReference.Should().NotBeNull();
            getSecretReference!.Should().DeepEqual(azGetSecretReference!);

            AssertKeyVaultReference(getSecretReference, "secretName", expectedSecretVersion);
            AssertKeyVaultReference(azGetSecretReference, "secretName", expectedSecretVersion);
            AssertKeyVaultReference(resourceGetSecretReference, "secretName", expectedSecretVersion);
        }
    }

    private static void AssertKeyVaultReference(JToken? reference, string expectedSecretName, string? expectedSecretVersion)
    {
        reference.Should().NotBeNull();
        reference!.SelectToken("$.keyVault.id").Should().NotBeNull();
        reference.SelectToken("$.secretName")!.Should().DeepEqual(expectedSecretName);
        if (expectedSecretVersion is null)
        {
            reference.SelectToken("$.secretVersion").Should().BeNull();
        }
        else
        {
            reference.SelectToken("$.secretVersion")!.Should().DeepEqual(expectedSecretVersion);
        }
    }

    [TestMethod]
    public void ExternalInput_without_config_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
param foo = externalInput('my.param.provider')
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Value.Should().BeNull();
        parameters["foo"].Expression.Should().DeepEqual("""[externalInputs('my_param_provider_0')]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["my_param_provider_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "my.param.provider",
        });
    }

    [TestMethod]
    public void ExternalInput_with_config_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
param foo = externalInput('sys.cli', 'foo')
param bar = externalInput('my.param.provider', {
    key: 'value'
    key2: 42
})
param baz = externalInput('my.provider', 42)
param qux = externalInput('is.this.a.provider', false)
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Value.Should().BeNull();
        parameters["foo"].Expression.Should().DeepEqual("""[externalInputs('sys_cli_0')]""");
        parameters["bar"].Value.Should().BeNull();
        parameters["bar"].Expression.Should().DeepEqual("""[externalInputs('my_param_provider_1')]""");
        parameters["baz"].Value.Should().BeNull();
        parameters["baz"].Expression.Should().DeepEqual("""[externalInputs('my_provider_2')]""");
        parameters["qux"].Value.Should().BeNull();
        parameters["qux"].Expression.Should().DeepEqual("""[externalInputs('is_this_a_provider_3')]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo",
        });
        externalInputs["my_param_provider_1"].Should().DeepEqual(new JObject
        {
            ["kind"] = "my.param.provider",
            ["config"] = new JObject
            {
                ["key"] = "value",
                ["key2"] = 42,
            },
        });
        externalInputs["my_provider_2"].Should().DeepEqual(new JObject
        {
            ["kind"] = "my.provider",
            ["config"] = 42,
        });
        externalInputs["is_this_a_provider_3"].Should().DeepEqual(new JObject
        {
            ["kind"] = "is.this.a.provider",
            ["config"] = false,
        });
    }

    [TestMethod]
    public void ExternalInput_parameter_with_variable_references_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
var foo = externalInput('sys.cli', 'foo')
var foo2 = '${foo}-${externalInput('sys.cli', 'foo2')}'
param foo3 = foo2
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo3"].Value.Should().BeNull();
        parameters["foo3"].Expression.Should().DeepEqual("""[format('{0}-{1}', externalInputs('sys_cli_0'), externalInputs('sys_cli_1'))]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo",
        });
        externalInputs["sys_cli_1"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo2",
        });
    }

    [TestMethod]
    public void ExternalInput_parameter_with_non_external_input_variable_references_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
var foo = 'foo'
param foo2 = '${foo}-${externalInput('sys.cli', 'foo2')}'
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo2"].Value.Should().BeNull();
        parameters["foo2"].Expression.Should().DeepEqual("""[format('{0}-{1}', 'foo', externalInputs('sys_cli_0'))]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo2",
        });
    }

    [TestMethod]
    public void ExternalInput_parameter_with_non_existent_symbol_reference_should_return_diagnostics()
    {
        var result = CompilationHelper.CompileParams(
("main.bicep", @"
"),
("parameters.bicepparam", @"
using none
import { foo } from 'main.bicep' // foo doesn't exist in main.bicep
param bar = '${foo}-${externalInput('test')}'
var baz = foo('test')
param qux = externalInput('kind', baz)
param bar2 = externalInput('kind', foo)
"));

        result.Should().HaveDiagnostics(
            [
                ("BCP360", DiagnosticLevel.Error, "The 'foo' symbol was not found in (or was not exported by) the imported template."),
                ("BCP063", DiagnosticLevel.Error, "The name \"foo\" is not a parameter, variable, resource or module."),
                ("BCP059", DiagnosticLevel.Error, "The name \"foo\" is not a function."),
                ("BCP062", DiagnosticLevel.Error, "The referenced declaration with name \"baz\" is not valid."),
                ("BCP063", DiagnosticLevel.Error, "The name \"foo\" is not a parameter, variable, resource or module."),
            ]);

    }

    [TestMethod]
    public void ExternalInput_parameter_with_non_pure_imported_variable_references_returns_diagnostic()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", @"
    using none
    import { foo } from 'main.bicep'
    param foo2 = '${foo}-${externalInput('sys.cli', 'foo2')}'
    "),
            ("main.bicep", @"
    @export()
    var foo = resourceGroup().location // cannot be evaluated in bicepparam file
    "));
        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"foo\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceGroup\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void Imported_variable_with_deployment_context_function_reference_returns_diagnostic_even_when_unused()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { defaultSubnetId } from 'variables.bicep'
                """),
            ("variables.bicep", """
                @export()
                var defaultSubnetId = resourceId('resourceGroup', 'Microsoft.Network/virtualNetworks/subnets', 'vnet', 'subnet')
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"defaultSubnetId\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void ImportedVariable_WithFullyQualifiedResourceId_Compiles()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { gatewayId } from 'variables.bicep'

                param value = gatewayId
                """),
            ("variables.bicep", """
                @export()
                var gatewayId = resourceId(
                  'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
                  'rg-test',
                  'Microsoft.HybridCompute/gateways',
                  'arc01-gw')
                """));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["value"].Value.Should().DeepEqual("/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/rg-test/providers/Microsoft.HybridCompute/gateways/arc01-gw");
    }

    [TestMethod]
    public void ImportedVariable_WithFullyQualifiedNestedResourceId_Compiles()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { subnetId } from 'variables.bicep'

                param value = subnetId
                """),
            ("variables.bicep", """
                @export()
                var subnetId = resourceId(
                  'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
                  'rg-test',
                  'Microsoft.Network/virtualNetworks/subnets',
                  'vnet',
                  'subnet')
                """));

        result.Should().NotHaveAnyDiagnostics();
    }

    [TestMethod]
    public void ImportedVariable_WithoutResourceGroupInResourceId_ReturnsDiagnostic()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { subnetId } from 'variables.bicep'
                """),
            ("variables.bicep", """
                @export()
                var subnetId = resourceId(
                  'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
                  'Microsoft.Network/virtualNetworks/subnets',
                  'vnet',
                  'subnet')
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"subnetId\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [DataTestMethod]
    [DataRow("subscriptionResourceId('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'Microsoft.Authorization/roleDefinitions', 'role')")]
    [DataRow("managementGroupResourceId('managementGroup', 'Microsoft.Authorization/policyDefinitions', 'policy')")]
    [DataRow("tenantResourceId('Microsoft.Authorization/policyDefinitions', 'policy')")]
    [DataRow("extensionResourceId('/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/rg-test', 'Microsoft.Authorization/locks', 'lock')")]
    public void ImportedVariable_WithContextIndependentResourceIdFunction_Compiles(string functionCall)
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { resourceIdValue } from 'variables.bicep'

                param value = resourceIdValue
                """),
            ("variables.bicep", $@"
                @export()
                var resourceIdValue = {functionCall}
                "));

        result.Should().NotHaveAnyDiagnostics();
    }

    [DataTestMethod]
    [DataRow("subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'role')", "subscriptionResourceId")]
    [DataRow("managementGroupResourceId('Microsoft.Authorization/policyDefinitions', 'policy')", "managementGroupResourceId")]
    public void ImportedVariable_WithContextDependentResourceIdFunction_ReturnsDiagnostic(string functionCall, string functionName)
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { resourceIdValue } from 'variables.bicep'
                """),
            ("variables.bicep", $@"
                @export()
                var resourceIdValue = {functionCall}
                "));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            $"The imported symbol \"resourceIdValue\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"{functionName}\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [DataTestMethod]
    [DataRow("extensionResourceId(resourceId('Microsoft.Storage/storageAccounts', 'storage'), 'Microsoft.Authorization/locks', 'lock')", "resourceId")]
    [DataRow("tenantResourceId('Microsoft.Authorization/policyDefinitions', resourceGroup().name)", "resourceGroup")]
    public void ImportedVariable_WithPureResourceIdFunctionWrappingContextDependentFunction_ReturnsDiagnostic(string functionCall, string functionName)
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { resourceIdValue } from 'variables.bicep'
                """),
            ("variables.bicep", $@"
                @export()
                var resourceIdValue = {functionCall}
                "));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            $"The imported symbol \"resourceIdValue\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"{functionName}\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void ImportedVariable_WithPureResourceIdFunctionUsingTransitivelyContextDependentArgument_ReturnsDiagnostic()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { lockId } from 'variables.bicep'
                """),
            ("variables.bicep", """
                var storageAccountId = resourceId('Microsoft.Storage/storageAccounts', 'storage')

                @export()
                var lockId = extensionResourceId(storageAccountId, 'Microsoft.Authorization/locks', 'lock')
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"lockId\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void Imported_variable_with_transitive_pure_function_reference_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { normalizedValue } from 'variables.bicep'

                param value = normalizedValue
                """),
            ("variables.bicep", """
                var normalizedValueCore = toLower('HELLO')

                @export()
                var normalizedValue = normalizedValueCore
                """));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["value"].Value.Should().DeepEqual("hello");
    }

    [TestMethod]
    public void Imported_variable_with_transitive_deployment_context_function_reference_returns_diagnostic_even_when_unused()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { defaultSubnetId } from 'variables.bicep'
                """),
            ("variables.bicep", """
                var defaultSubnetIdCore = resourceId('resourceGroup', 'Microsoft.Network/virtualNetworks/subnets', 'vnet', 'subnet')

                @export()
                var defaultSubnetId = defaultSubnetIdCore
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"defaultSubnetId\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void Wildcard_import_with_deployment_context_function_reference_returns_diagnostic_even_when_unused()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import * as values from 'variables.bicep'
                """),
            ("variables.bicep", """
                @export()
                var defaultSubnetId = resourceId('resourceGroup', 'Microsoft.Network/virtualNetworks/subnets', 'vnet', 'subnet')
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"values\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void Wildcard_import_with_deployment_context_function_reference_returns_diagnostic_for_pure_member_usage()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import * as values from 'variables.bicep'

                param value = values.safeValue
                """),
            ("variables.bicep", """
                @export()
                var safeValue = toLower('HELLO')

                @export()
                var defaultSubnetId = resourceId('resourceGroup', 'Microsoft.Network/virtualNetworks/subnets', 'vnet', 'subnet')
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"values\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceId\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void ExternalInput_parameter_with_imported_function_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
    using none
    import { foo } from 'main.bicep'
    param foo2 = '${foo()}-${externalInput('sys.cli', 'foo2')}'
    "),
("main.bicep", @"
    @export()
    func foo() string => 'Hello foo'
    "));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo2"].Value.Should().BeNull();
        parameters["foo2"].Expression.Should().DeepEqual("""[format('{0}-{1}', 'Hello foo', externalInputs('sys_cli_0'))]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo2",
        });
    }

    [TestMethod]
    public void Imported_function_with_transitive_pure_function_references_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { normalize } from 'functions.bicep'

                param value = normalize('HELLO')
                """),
            ("functions.bicep", """
                func normalizeCore(value string) string => toLower(value)

                @export()
                func normalize(value string) string => normalizeCore(value)
                """));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["value"].Value.Should().DeepEqual("hello");
    }

    [TestMethod]
    public void Imported_function_with_transitive_deployment_context_function_reference_returns_diagnostic_even_when_unused()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", """
                using none
                import { getLocation } from 'functions.bicep'
                """),
            ("functions.bicep", """
                func getLocationCore() string => resourceGroup().location

                @export()
                func getLocation() string => getLocationCore()
                """));

        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"getLocation\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceGroup\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void ExternalInput_parameter_with_non_pure_imported_function_returns_diagnostics()
    {
        var result = CompilationHelper.CompileParams(
            ("parameters.bicepparam", @"
    using none
    import { foo } from 'main.bicep'
    param foo2 = '${foo()}-${externalInput('sys.cli', 'foo2')}'
    "),
            ("main.bicep", @"
    @export()
    func foo() string => resourceGroup().location // cannot be evaluated in bicepparam file
    "));
        result.Should().OnlyContainDiagnostic(
            "BCP452",
            DiagnosticLevel.Error,
            "The imported symbol \"foo\" cannot be used in a .bicepparam file because it depends on deployment-context functions: \"resourceGroup\". Imported declarations may only use functions that can be evaluated while building the parameters file.");
    }

    [TestMethod]
    public void ExternalInput_parameter_with_param_references_compiles_successfully()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
param foo = externalInput('sys.cli', 'foo')
param foo2 = foo
param foo3 = foo2
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Value.Should().BeNull();
        parameters["foo"].Expression.Should().DeepEqual("""[externalInputs('sys_cli_0')]""");

        parameters["foo2"].Value.Should().BeNull();
        parameters["foo2"].Expression.Should().DeepEqual("""[externalInputs('sys_cli_0')]""");

        parameters["foo3"].Value.Should().BeNull();
        parameters["foo3"].Expression.Should().DeepEqual("""[externalInputs('sys_cli_0')]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cli",
            ["config"] = "foo",
        });
    }

    [TestMethod]
    public void ExternalInput_parameter_with_cyclic_references_returns_errors()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
param a = '${b}-${externalInput('sys.cli', 'a')}'
var b = '${c}-${a}'
param c = b
"));

        result.Should().NotGenerateParameters();
        result.Should().HaveDiagnostics(new[]
        {
            ("BCP062", DiagnosticLevel.Error, """The referenced declaration with name "b" is not valid."""),
            ("BCP080", DiagnosticLevel.Error, """The expression is involved in a cycle ("c" -> "b")."""),
            ("BCP080", DiagnosticLevel.Error, """The expression is involved in a cycle ("a" -> "b")."""),
            ("BCP080", DiagnosticLevel.Error, """The expression is involved in a cycle ("b" -> "c")."""),
        });
    }

    [TestMethod]
    public void ExternalInput_non_compile_time_constant_is_blocked()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
var myVar = 2 + 3
param foo = externalInput('sys.cli', myVar)
"));

        result.Should().NotGenerateParameters();
        result.Should().HaveDiagnostics(new[]
        {
            ("BCP032", DiagnosticLevel.Error, "The value must be a compile-time constant."),
        });
    }

    [TestMethod]
    public void ExternalInput_emits_top_level_expression()
    {
        var result = CompilationHelper.CompileParams(
("parameters.bicepparam", @"
using none
param foo = {
  bar: externalInput('my.param.provider')
}
"));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo"].Value.Should().BeNull();
        parameters["foo"].Expression.Should().DeepEqual("""[createObject('bar', externalInputs('my_param_provider_0'))]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["my_param_provider_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "my.param.provider",
        });
    }

    [TestMethod]
    public void ExternalInput_alternative_functions_also_generate_external_inputs()
    {
        var services = new ServiceBuilder().WithFeatureOverrides(new(TestContext, DeployCommandsEnabled: true));
        var result = CompilationHelper.CompileParams(
            services,
            ("parameters.bicepparam", """
                using 'main.bicep' with {
                  mode: 'deployment'
                  scope: '/subscriptions/foo/resourceGroups/bar'
                }
                var foo = readCliArg('foo')
                var foo2 = '${foo}-${readEnvVar('foo2')}'
                param foo3 = foo2
                """), ("main.bicep", """
                param foo3 string
                """));

        result.Should().NotHaveAnyDiagnostics();
        var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
        parameters["foo3"].Value.Should().BeNull();
        parameters["foo3"].Expression.Should().DeepEqual("""[format('{0}-{1}', externalInputs('sys_cliArg_0'), externalInputs('sys_envVar_1'))]""");

        var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
        externalInputs["sys_cliArg_0"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.cliArg",
            ["config"] = "foo",
        });
        externalInputs["sys_envVar_1"].Should().DeepEqual(new JObject
        {
            ["kind"] = "sys.envVar",
            ["config"] = "foo2",
        });
    }
}
