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

using System.Collections.Immutable;
using Azure.Bicep.Types.Az;
using Bicep.Core.Configuration;
using Bicep.Core.Diagnostics;
using Bicep.Core.Extensions;
using Bicep.Core.Features;
using Bicep.Core.IntegrationTests.Extensibility;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Namespaces;
using Bicep.Core.SourceGraph;
using Bicep.Core.TypeSystem;
using Bicep.Core.TypeSystem.Providers;
using Bicep.Core.TypeSystem.Providers.Az;
using Bicep.Core.TypeSystem.Types;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Mock;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json.Linq;

namespace Bicep.Core.IntegrationTests
{
    [TestClass]
    public class ExtensionImportTests : TestBase
    {
        private static readonly Lazy<IResourceTypeLoader> azTypeLoaderLazy = new(() => new AzResourceTypeLoader(new AzTypeLoader()));

        private async Task<ServiceBuilder> GetServices()
        {
            var fileSystem = new Dictionary<string, string>
            {
                ["/types/index.json"] = """{"resources": {}, "resourceFunctions": {}, "namespaceFunctions": []}""",
            };

            var services = new ServiceBuilder()
                .WithContainerRegistryClientFactory(RegistryHelper.CreateOciClientForAzExtension())
                .WithMockFileSystem(fileSystem)
                .WithAzResourceTypeLoader(azTypeLoaderLazy.Value);

            await RegistryHelper.PublishAzExtension(services.Build(), "/types/index.json");

            return services;
        }

        [TestMethod]
        public async Task Extension_Statement_Without_Specification_String_Should_Emit_Diagnostic()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, @"
extension
");
            result.Should().HaveDiagnostics([
                ("BCP201", DiagnosticLevel.Error, """Expected an extension specification string. This should either be a relative path, or a valid OCI artifact specification."""),
            ]);
        }

        [TestMethod]
        public async Task Extension_Statement_With_Invalid_Keyword_Should_Emit_Diagnostic()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension sys blahblah
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP305", DiagnosticLevel.Error, "Expected the \"with\" keyword, \"as\" keyword, or a new line character at this location."),
            });
        }

        [TestMethod]
        public async Task Provider_Statement_Without_Brace_Should_Raise_Error()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension kubernetes with
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP018", DiagnosticLevel.Error, "Expected the \"{\" character at this location."),
            });
        }

        [TestMethod]
        public async Task Provider_Statement_Without_As_Keyword_Should_Raise_Error()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension kubernetes with {
                kubeConfig: 'foo'
                namespace: 'bar'
            } something
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP305", DiagnosticLevel.Error, """Expected the "with" keyword, "as" keyword, or a new line character at this location."""),
            });
        }

        [TestMethod]
        public async Task Provider_Statement_Without_Alias_Name_Should_Raise_Error()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension kubernetes with {
                kubeConfig: 'foo'
                namespace: 'bar'
            } as
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP202", DiagnosticLevel.Error, "Expected an extension alias name at this location."),
            });
        }

        [TestMethod]
        public async Task Provider_Statement_Without_Alias_Name_For_Sys_Should_Raise_Error()
        {
            var services = await GetServices();
            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension sys as
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP202", DiagnosticLevel.Error, "Expected an extension alias name at this location."),
            });
        }

        [TestMethod]
        public async Task Import_configuration_is_blocked_by_default()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az with {
              foo: 'bar'
            }
            """);
            result.Should().HaveDiagnostics(new[] {
                ("BCP205", DiagnosticLevel.Error, "Extension \"az\" does not support configuration."),
            });
        }

        [TestMethod]
        public async Task Az_extension_config_is_supported_when_feature_is_enabled()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['Microsoft.Storage', 'Microsoft.Compute']
            }
            """);

            result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();

            // Emits under the classic imports path (no moduleExtensionConfigs). Verify the version
            // matches the version derived from the embedded Azure.Bicep.Types.Az assembly, and that
            // the shipped provider list is faithfully round-tripped into the template.
            result.Template.Should().NotBeNull();
            result.Template.Should().HaveValueAtPath("$.imports.az.version", AzNamespaceType.Settings.TemplateExtensionVersion);
            result.Template.Should().HaveValueAtPath("$.imports.az.config.providers", JArray.Parse("""["Microsoft.Storage", "Microsoft.Compute"]"""));
        }

        [TestMethod]
        public async Task Az_extension_config_with_module_extension_configs_emits_under_extensions_path()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true, ModuleExtensionConfigsEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['Microsoft.Storage', 'Microsoft.Compute']
            }
            """);

            result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();

            // With moduleExtensionConfigs enabled the extension is emitted under $.extensions instead
            // of $.imports, each config property wraps its value in { defaultValue: <expr> }, and the
            // template opts into the experimental languageVersion.
            result.Template.Should().NotBeNull();
            result.Template.Should().HaveValueAtPath("$.extensions.az.name", "AzureResourceManager");
            result.Template.Should().HaveValueAtPath("$.extensions.az.version", AzNamespaceType.Settings.TemplateExtensionVersion);
            result.Template.Should().HaveValueAtPath("$.extensions.az.config.providers.defaultValue", JArray.Parse("""["Microsoft.Storage", "Microsoft.Compute"]"""));
            result.Template.Should().NotHaveValueAtPath("$.imports.az");
        }

        [TestMethod]
        public async Task Az_extension_config_with_module_extension_configs_rejects_unknown_provider()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true, ModuleExtensionConfigsEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['NotARealProvider']
            }
            """);

            // Validation of the string-literal union should behave identically regardless of which
            // emit path (imports vs. extensions) is in use.
            result.ExcludingLinterDiagnostics().Diagnostics
                .Should().Contain(d => d.Code == "BCP034", "unknown providers should still surface as BCP034 even under moduleExtensionConfigs");
        }

        [TestMethod]
        public async Task Az_extension_config_does_not_change_resource_emit_under_imports_path()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['Microsoft.Storage']
            }

            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
              name: 'mystorage'
              location: 'eastus'
              sku: { name: 'Standard_LRS' }
              kind: 'StorageV2'
            }
            """);

            result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
            result.Template.Should().NotBeNull();

            // The extension config appears where expected...
            result.Template.Should().HaveValueAtPath("$.imports.az.config.providers", JArray.Parse("""["Microsoft.Storage"]"""));

            // ...and the resource is still emitted with all top-level fields intact. Under the
            // imports path resources are keyed by symbolic name and carry `import: 'az'` so the
            // deployment engine knows which import owns them.
            result.Template.Should().HaveValueAtPath("$.resources.storage.import", "az");
            result.Template.Should().HaveValueAtPath("$.resources.storage.type", "Microsoft.Storage/storageAccounts");
            result.Template.Should().HaveValueAtPath("$.resources.storage.apiVersion", "2023-01-01");
            result.Template.Should().HaveValueAtPath("$.resources.storage.name", "mystorage");
            result.Template.Should().HaveValueAtPath("$.resources.storage.location", "eastus");
            result.Template.Should().HaveValueAtPath("$.resources.storage.kind", "StorageV2");
            result.Template.Should().HaveValueAtPath("$.resources.storage.sku.name", "Standard_LRS");
        }

        [TestMethod]
        public async Task Az_extension_config_does_not_change_resource_emit_under_extensions_path()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true, ModuleExtensionConfigsEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['Microsoft.Storage']
            }

            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
              name: 'mystorage'
              location: 'eastus'
              sku: { name: 'Standard_LRS' }
              kind: 'StorageV2'
            }
            """);

            result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics();
            result.Template.Should().NotBeNull();

            // The extension config appears under $.extensions with the defaultValue wrapper...
            result.Template.Should().HaveValueAtPath("$.extensions.az.config.providers.defaultValue", JArray.Parse("""["Microsoft.Storage"]"""));

            // ...and the resource is emitted under $.resources keyed by symbolic name, tagged with
            // `extension: 'az'` (matching the extension key under $.extensions). Core resource fields
            // are preserved at the top level exactly as they'd be without any config wiring.
            result.Template.Should().HaveValueAtPath("$.resources.storage.extension", "az");
            result.Template.Should().HaveValueAtPath("$.resources.storage.type", "Microsoft.Storage/storageAccounts");
            result.Template.Should().HaveValueAtPath("$.resources.storage.apiVersion", "2023-01-01");
            result.Template.Should().HaveValueAtPath("$.resources.storage.name", "mystorage");
            result.Template.Should().HaveValueAtPath("$.resources.storage.location", "eastus");
            result.Template.Should().HaveValueAtPath("$.resources.storage.kind", "StorageV2");
            result.Template.Should().HaveValueAtPath("$.resources.storage.sku.name", "Standard_LRS");
        }

        [TestMethod]
        public async Task Az_extension_config_rejects_unknown_provider_when_feature_is_enabled()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['NotARealProvider']
            }
            """);

            // 'NotARealProvider' is not a member of the string-literal union shipped in bicep-types-az.
            // Asserted by code only because the full message enumerates ~283 provider namespaces and would
            // churn every time bicep-types-az adds one.
            result.ExcludingLinterDiagnostics().Diagnostics
                .Should().Contain(d => d.Code == "BCP034", "unknown providers should surface as BCP034 array-item-type mismatch");
        }

        [TestMethod]
        public async Task Az_extension_config_rejects_unknown_property_when_feature_is_enabled()
        {
            var services = (await GetServices())
                .WithFeatureOverrides(new(AzExtensionConfigEnabled: true));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension az with {
              providers: ['Microsoft.Storage']
              bogus: true
            }
            """);

            // The configuration object type declares only 'providers'; extras should surface as BCP037.
            result.ExcludingLinterDiagnostics().Diagnostics
                .Should().Contain(d => d.Code == "BCP037", "unknown properties on the az configuration should surface as BCP037");
        }

        [TestMethod]
        public async Task Imports_return_error_with_unrecognized_namespace()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), @"
extension madeUpNamespace
");
            result.Should().HaveDiagnostics(new[] {
                ("BCP204", DiagnosticLevel.Error, "Extension \"madeUpNamespace\" is not recognized."),
            });
        }

        [TestMethod]
        public async Task Using_import_statements_frees_up_the_namespace_symbol()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az as newAz

            var az = 'Fake AZ!'
            var myRg = newAz.resourceGroup()

            output az string = az
            output rgLocation string = myRg.location
            """);

            result.Should().NotHaveAnyDiagnostics();
        }

        [TestMethod]
        public async Task You_can_swap_imported_namespaces_if_you_really_really_want_to()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az as sys
            extension sys as az

            var myRg = sys.resourceGroup()

            @az.description('why on earth would you do this?')
            output rgLocation string = myRg.location
            """);

            result.Should().GenerateATemplate();
            result.Should().NotHaveAnyDiagnostics();
            result.Template.Should().HaveValueAtPath("$.outputs.rgLocation.metadata.description", "why on earth would you do this?");
        }

        [TestMethod]
        public async Task Overwriting_single_built_in_namespace_with_import_is_prohibited()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az as sys

            var myRg = sys.resourceGroup()

            output rgLocation string = myRg.location
            """);

            result.Should().ContainDiagnostic("BCP084", DiagnosticLevel.Error, "The symbolic name \"sys\" is reserved. Please use a different symbolic name. Reserved namespaces are \"sys\".");
        }

        [TestMethod]
        public async Task Singleton_imports_cannot_be_used_multiple_times()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az as az1
            extension az as az2

            extension sys as sys1
            extension sys as sys2
            """);

            result.Should().HaveDiagnostics(new[] {
                ("BCP207", DiagnosticLevel.Error, "Namespace \"az\" is declared multiple times. Remove the duplicates."),
                ("BCP207", DiagnosticLevel.Error, "Namespace \"az\" is declared multiple times. Remove the duplicates."),
                ("BCP207", DiagnosticLevel.Error, "Namespace \"sys\" is declared multiple times. Remove the duplicates."),
                ("BCP207", DiagnosticLevel.Error, "Namespace \"sys\" is declared multiple times. Remove the duplicates."),
            });
        }

        [TestMethod]
        public async Task Import_names_must_not_conflict_with_other_symbols()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), """
            extension az
            extension kubernetes with {
            kubeConfig: ''
            namespace: ''
            } as az
            """);

            result.Should().HaveDiagnostics(new[] {
                ("BCP028", DiagnosticLevel.Error, "Identifier \"az\" is declared multiple times. Remove or rename the duplicates."),
                ("BCP028", DiagnosticLevel.Error, "Identifier \"az\" is declared multiple times. Remove or rename the duplicates."),
            });
        }

        [TestMethod]
        public async Task Ambiguous_function_references_must_be_qualified()
        {
            var ns1 = new NamespaceType(
                "ns1",
                new NamespaceSettings(
                    IsSingleton: true,
                    BicepExtensionName: "ns1",
                    ConfigurationType: null,
                    TemplateExtensionName: "Ns1-Unused",
                    TemplateExtensionVersion: "1.0"),
                ImmutableArray<NamedTypeProperty>.Empty,
                new[] {
                    new FunctionOverloadBuilder("ns1Func").Build(),
                    new FunctionOverloadBuilder("dupeFunc").Build(),
                },
                ImmutableArray<BannedFunction>.Empty,
                ImmutableArray<Decorator>.Empty,
                new EmptyResourceTypeProvider());

            var ns2 = new NamespaceType(
                "ns2",
                new NamespaceSettings(
                    IsSingleton: true,
                    BicepExtensionName: "ns2",
                    ConfigurationType: null,
                    TemplateExtensionName: "Ns2-Unused",
                    TemplateExtensionVersion: "1.0"),
                ImmutableArray<NamedTypeProperty>.Empty,
                new[] {
                    new FunctionOverloadBuilder("ns2Func").Build(),
                    new FunctionOverloadBuilder("dupeFunc").Build(),
                },
                ImmutableArray<BannedFunction>.Empty,
                ImmutableArray<Decorator>.Empty,
                new EmptyResourceTypeProvider());

            var nsProvider = TestExtensionsNamespaceProvider.Create((extensionName, aliasName) => extensionName switch
            {
                "ns1" => ns1,
                "ns2" => ns2,
                _ => null,
            });

            var services = (await GetServices())
                .WithNamespaceProvider(nsProvider)
                .WithConfigurationPatch(c => c.WithExtensions("""
                {
                  "az": "builtin:",
                  "ns1": "builtin:",
                  "ns2": "builtin:"
                }
                """));

            var result = await CompilationHelper.RestoreAndCompile(services, @"
            extension ns1
            extension ns2

            output ambiguousResult string = dupeFunc()
            output ns1Result string = ns1Func()
            output ns2Result string = ns2Func()
            ");

            result.Should().HaveDiagnostics(new[] {
                ("BCP056", DiagnosticLevel.Error, "The reference to name \"dupeFunc\" is ambiguous because it exists in namespaces \"ns1\", \"ns2\". The reference must be fully-qualified."),
            });

            // fix by fully-qualifying
            result = await CompilationHelper.RestoreAndCompile(services, @"
            extension ns1
            extension ns2

            output ambiguousResult string = ns1.dupeFunc()
            output ns1Result string = ns1Func()
            output ns2Result string = ns2Func()
            ");

            result.Should().NotHaveAnyDiagnostics();
        }

        [TestMethod]
        public async Task Config_with_optional_properties_can_be_skipped()
        {
            var mockNs = new NamespaceType(
                "mockNs",
                new(
                    IsSingleton: false,
                    BicepExtensionName: "mockNs",
                    ConfigurationType: new ObjectType(
                        "mockNs",
                        TypeSymbolValidationFlags.Default,
                        new[]
                        {
                            new NamedTypeProperty("optionalConfig", LanguageConstants.String, TypePropertyFlags.DeployTimeConstant),
                        },
                        null),
                    TemplateExtensionName: "Unused",
                    TemplateExtensionVersion: "1.0.0"),
                ImmutableArray<NamedTypeProperty>.Empty,
                ImmutableArray<FunctionOverload>.Empty,
                ImmutableArray<BannedFunction>.Empty,
                ImmutableArray<Decorator>.Empty,
                new EmptyResourceTypeProvider());

            var nsProvider = TestExtensionsNamespaceProvider.Create((extensionName, aliasName) => extensionName switch
            {
                "mockNs" => mockNs,
                _ => null,
            });

            var services = (await GetServices())
                .WithNamespaceProvider(nsProvider)
                .WithConfigurationPatch(c => c.WithExtensions("""
                {
                  "az": "builtin:",
                  "mockNs": "builtin:"
                }
                """));

            var result = await CompilationHelper.RestoreAndCompile(services, """
            extension mockNs with {
              optionalConfig: 'blah blah'
            } as ns1
            extension mockNs
            """);

            result.Should().NotHaveAnyDiagnostics();
        }

        [TestMethod]
        public async Task MicrosoftGraph_imports_succeed_default()
        {
            var result = await CompilationHelper.RestoreAndCompile(await GetServices(), @"extension microsoftGraph as graph");

            result.Should().NotGenerateATemplate();
            result.Should().HaveDiagnostics(new[] {
                ("BCP407", DiagnosticLevel.Error, """Built-in extension "microsoftGraph" is retired. Use dynamic types instead. See https://aka.ms/graphbicep/dynamictypes"""),
            });
        }
    }
}
