﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Resources;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Diagnostics
{
    public partial class DiagnosticLocalizationTests
    {
        [Fact, WorkItem(1006, "https://github.com/dotnet/roslyn/issues/1006")]
        public void TestDiagnosticLocalization()
        {
            var resourceManager = GetTestResourceManagerInstance();
            var arCulture = CultureInfo.CreateSpecificCulture("ar-SA");
            var defaultCultureResourceSet = resourceManager.GetResourceSet(CustomResourceManager.DefaultCulture, false, false);
            var arResourceSet = resourceManager.GetResourceSet(arCulture, false, false);

            var nameOfResource1 = @"Resource1";
            var nameOfResource2 = @"Resource2";
            var nameOfResource3 = @"Resource3";

            var fixedTitle = defaultCultureResourceSet.GetString(nameOfResource1);
            var fixedMessageFormat = defaultCultureResourceSet.GetString(nameOfResource2);
            var fixedDescription = defaultCultureResourceSet.GetString(nameOfResource3);

            var localizedTitle = arResourceSet.GetString(nameOfResource1);
            var localizedMessageFormat = arResourceSet.GetString(nameOfResource2);
            var localizedDescription = arResourceSet.GetString(nameOfResource3);

            // Test descriptor localization.

            // Test non-localizable title, description and message.
            var descriptor = new DiagnosticDescriptor(
                "Id",
                fixedTitle,
                fixedMessageFormat,
                "Category",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: fixedDescription);

            Assert.Equal(fixedTitle, descriptor.Title.ToString(arCulture));
            Assert.Equal(fixedMessageFormat, descriptor.MessageFormat.ToString(arCulture));
            Assert.Equal(fixedDescription, descriptor.Description.ToString(arCulture));

            // Test localizable title, description and message.
            var localizableTitle = new LocalizableResourceString(nameOfResource1, resourceManager, typeof(CustomResourceManager));
            var localizableMessageFormat = new LocalizableResourceString(nameOfResource2, resourceManager, typeof(CustomResourceManager));
            var localizableDescription = new LocalizableResourceString(nameOfResource3, resourceManager, typeof(CustomResourceManager));

            descriptor = new DiagnosticDescriptor(
                "Id",
                localizableTitle,
                localizableMessageFormat,
                "Category",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: localizableDescription);

            if (EnsureEnglishUICulture.PreferredOrNull == null)
            {
                Assert.Equal<string>(fixedTitle, descriptor.Title.ToString());
                Assert.Equal<string>(fixedMessageFormat, descriptor.MessageFormat.ToString());
                Assert.Equal<string>(fixedDescription, descriptor.Description.ToString());
            }

            Assert.Equal<string>(fixedTitle, descriptor.Title.ToString(CustomResourceManager.DefaultCulture));
            Assert.Equal<string>(fixedMessageFormat, descriptor.MessageFormat.ToString(CustomResourceManager.DefaultCulture));
            Assert.Equal<string>(fixedDescription, descriptor.Description.ToString(CustomResourceManager.DefaultCulture));

            Assert.Equal(localizedTitle, descriptor.Title.ToString(arCulture));
            Assert.Equal(localizedMessageFormat, descriptor.MessageFormat.ToString(arCulture));
            Assert.Equal(localizedDescription, descriptor.Description.ToString(arCulture));

            // Test diagnostic localization.
            var localizableDiagnostic = Diagnostic.Create(descriptor, Location.None);

            if (EnsureEnglishUICulture.PreferredOrNull == null)
            {
                // Test non-localized title, description and message.
                Assert.Equal(fixedTitle, localizableDiagnostic.Descriptor.Title.ToString());
                Assert.Equal(fixedMessageFormat, localizableDiagnostic.GetMessage());
                Assert.Equal(fixedDescription, localizableDiagnostic.Descriptor.Description.ToString());
            }

            Assert.Equal(fixedTitle, localizableDiagnostic.Descriptor.Title.ToString(CustomResourceManager.DefaultCulture));
            Assert.Equal(fixedMessageFormat, localizableDiagnostic.GetMessage(CustomResourceManager.DefaultCulture));
            Assert.Equal(fixedDescription, localizableDiagnostic.Descriptor.Description.ToString(CustomResourceManager.DefaultCulture));

            // Test localized title, description and message.
            Assert.Equal(localizedTitle, localizableDiagnostic.Descriptor.Title.ToString(arCulture));
            Assert.Equal(localizedMessageFormat, localizableDiagnostic.GetMessage(arCulture));
            Assert.Equal(localizedDescription, localizableDiagnostic.Descriptor.Description.ToString(arCulture));

            // Test argument formatting for localized string
            var nameOfResourceWithArguments = @"ResourceWithArguments";
            var argument = "formatted";
            var localizableResource = new LocalizableResourceString(nameOfResourceWithArguments, resourceManager, typeof(CustomResourceManager), argument);

            // Verify without culture
            var defaultCultureLocalizedStringWithArguments = defaultCultureResourceSet.GetString(nameOfResourceWithArguments);
            var expected = string.Format(defaultCultureLocalizedStringWithArguments, argument);

            if (EnsureEnglishUICulture.PreferredOrNull == null)
            {
                Assert.Equal(expected, localizableResource.ToString());
            }

            Assert.Equal(expected, localizableResource.ToString(CustomResourceManager.DefaultCulture));

            // Verify with loc culture
            var arLocalizedStringWithArguments = arResourceSet.GetString(nameOfResourceWithArguments);
            expected = string.Format(arLocalizedStringWithArguments, argument);
            Assert.Equal(expected, localizableResource.ToString(arCulture));
        }

        private static CustomResourceManager GetTestResourceManagerInstance()
        {
            var defaultCultureResources = new Dictionary<string, string>()
                {
                    { "Resource1", "My Resource 1 DefaultCulture string" },
                    { "Resource2", "My Resource 2 DefaultCulture string" },
                    { "Resource3", "My Resource 3 DefaultCulture string" },
                    { "ResourceWithArguments", "My Resource DefaultCulture string {0}" }
                };

            var arResources = new Dictionary<string, string>()
                {
                    { "Resource1", "ARABIC string for My Resource 1" },
                    { "Resource2", "ARABIC string for My Resource 2" },
                    { "Resource3", "ARABIC string for My Resource 3" },
                    { "ResourceWithArguments", "{0} ARABIC string for My Resource" }
                };

            var resourceSetMap = new Dictionary<string, Dictionary<string, string>>()
                {
                    { CustomResourceManager.DefaultCulture.Name, defaultCultureResources },
                    { "ar-SA", arResources }
                };

            return new CustomResourceManager(resourceSetMap);
        }

        private class CustomResourceManager : ResourceManager
        {
            private readonly Dictionary<string, CustomResourceSet> _resourceSetMap;

            public CustomResourceManager(Dictionary<string, CustomResourceSet> resourceSetMap)
            {
                _resourceSetMap = resourceSetMap;
            }

            public CustomResourceManager(Dictionary<string, Dictionary<string, string>> resourceSetMap)
            {
                _resourceSetMap = new Dictionary<string, CustomResourceSet>();

                foreach (var kvp in resourceSetMap)
                {
                    var resourceSet = new CustomResourceSet(kvp.Value);
                    _resourceSetMap.Add(kvp.Key, resourceSet);
                }
            }

            public static CultureInfo DefaultCulture => CultureInfo.CurrentUICulture;

            public void VerifyResourceValue(string resourceName, string cultureName, string expectedResourceValue)
            {
                var actual = this.GetString(resourceName, CultureInfo.CreateSpecificCulture(cultureName));
                Assert.Equal(expectedResourceValue, actual);
            }

            public override string GetString(string name, CultureInfo culture)
            {
                return GetResourceSet(culture, false, false).GetString(name);
            }

            public override ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
            {
                return _resourceSetMap[culture.Name];
            }

            public override string GetString(string name)
            {
                return GetString(name, CultureInfo.InvariantCulture);
            }

            public override object GetObject(string name)
            {
                return GetString(name);
            }

            public override object GetObject(string name, CultureInfo culture)
            {
                return GetString(name, culture);
            }

            public class CustomResourceSet : ResourceSet
            {
                private readonly Dictionary<string, string> _resourcesMap;
                public CustomResourceSet(Dictionary<string, string> resourcesMap)
                {
                    _resourcesMap = resourcesMap;
                }

                public override string GetString(string name)
                {
                    return _resourcesMap[name];
                }

                public override string GetString(string name, bool ignoreCase)
                {
                    throw new NotImplementedException();
                }

                public override object GetObject(string name)
                {
                    return GetString(name);
                }

                public override object GetObject(string name, bool ignoreCase)
                {
                    throw new NotImplementedException();
                }
            }
        }

        [Fact]
        public void LocalizableResourceStringEquality()
        {
            var resourceManager = GetTestResourceManagerInstance();
            var unit = EqualityUnit
                .Create(new LocalizableResourceString(@"ResourceWithArguments", resourceManager, typeof(CustomResourceManager), "arg"))
                .WithEqualValues(
                    new LocalizableResourceString(@"ResourceWithArguments", resourceManager, typeof(CustomResourceManager), "arg"))
                .WithNotEqualValues(
                    new LocalizableResourceString(@"ResourceWithArguments", resourceManager, typeof(CustomResourceManager), "otherarg"),
                    new LocalizableResourceString(@"Resource1", resourceManager, typeof(CustomResourceManager)));
            EqualityUtil.RunAll(unit, checkIEquatable: false);

            var str = new LocalizableResourceString(@"ResourceWithArguments", resourceManager, typeof(CustomResourceManager), "arg");
            var threw = false;
            str.OnException += (sender, e) => { threw = true; };
            Assert.False(str.Equals(42));
            Assert.False(str.Equals(42));
            Assert.False(threw);
        }

        [Fact, WorkItem(887, "https://github.com/dotnet/roslyn/issues/887")]
        public void TestDescriptorIsExceptionSafe()
        {
            // Test descriptor with LocalizableResourceString fields that can throw.
            var descriptor1 = GetDescriptorWithLocalizableResourceStringsThatThrow();
            TestDescriptorIsExceptionSafeCore(descriptor1);

            // Test descriptor with Custom implemented LocalizableString fields that can throw.
            var descriptor2 = GetDescriptorWithCustomLocalizableStringsThatThrow();
            TestDescriptorIsExceptionSafeCore(descriptor2);

            // Also verify exceptions from Equals and GetHashCode don't go unhandled.
            var unused1 = descriptor2.Title.GetHashCode();
            var unused2 = descriptor2.Equals(descriptor1);
        }

        private static void TestDescriptorIsExceptionSafeCore(DiagnosticDescriptor descriptor)
        {
            var localizableTitle = descriptor.Title;
            var localizableMessage = descriptor.MessageFormat;
            var localizableDescription = descriptor.Description;

            // Verify exceptions from LocalizableResourceString don't go unhandled.
            var title = localizableTitle.ToString();
            var message = localizableMessage.ToString();
            var description = localizableDescription.ToString();

            // Verify exceptions from LocalizableResourceString are raised if OnException is set.
            var exceptions = new List<Exception>();
            var handler = new EventHandler<Exception>((sender, ex) => exceptions.Add(ex));
            localizableTitle.OnException += handler;
            localizableMessage.OnException += handler;
            localizableDescription.OnException += handler;

            // Access and evaluate localizable fields.
            var unused1 = localizableTitle.ToString();
            var unused2 = localizableMessage.ToString();
            var unused3 = localizableDescription.ToString();

            Assert.Equal(3, exceptions.Count);

            // Verify DiagnosticAnalyzer.SupportedDiagnostics is also exception safe.
            var analyzer = new MyAnalyzer(descriptor);
            var exceptionDiagnostics = new List<Diagnostic>();

            Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> onAnalyzerException = (ex, a, diag, ct) => exceptionDiagnostics.Add(diag);
            var analyzerManager = new AnalyzerManager(analyzer);
            var compilation = CSharp.CSharpCompilation.Create("test");
            var analyzerExecutor = AnalyzerExecutor.Create(compilation, AnalyzerOptions.Empty,
                addNonCategorizedDiagnostic: (_, _) => { }, onAnalyzerException, analyzerExceptionFilter: null,
                isCompilerAnalyzer: _ => false, analyzerManager, shouldSkipAnalysisOnGeneratedCode: _ => false,
                shouldSuppressGeneratedCodeDiagnostic: (_, _, _, _) => false, isGeneratedCodeLocation: (_, _, _) => false,
                isAnalyzerSuppressedForTree: (_, _, _, _) => false, getAnalyzerGate: _ => null,
                getSemanticModel: tree => compilation.GetSemanticModel(tree, ignoreAccessibility: true));
            var descriptors = analyzerManager.GetSupportedDiagnosticDescriptors(analyzer, analyzerExecutor, CancellationToken.None);

            Assert.Equal(1, descriptors.Length);
            Assert.Equal(descriptor.Id, descriptors[0].Id);

            // Access and evaluate localizable fields.
            unused1 = descriptors[0].Title.ToString();
            unused2 = descriptors[0].MessageFormat.ToString();
            unused3 = descriptors[0].Description.ToString();

            // Verify logged analyzer exception diagnostics.
            Assert.Equal(3, exceptionDiagnostics.Count);
            Assert.True(exceptionDiagnostics.TrueForAll(AnalyzerExecutor.IsAnalyzerExceptionDiagnostic));
        }

        private static DiagnosticDescriptor GetDescriptorWithLocalizableResourceStringsThatThrow()
        {
            var resourceManager = GetTestResourceManagerInstance();

            // Test localizable title that throws.
            var localizableTitle = new LocalizableResourceString("NonExistentTitleResourceName", resourceManager, typeof(CustomResourceManager));
            var localizableMessage = new LocalizableResourceString("NonExistentMessageResourceName", resourceManager, typeof(CustomResourceManager));
            var localizableDescription = new LocalizableResourceString("NonExistentDescriptionResourceName", resourceManager, typeof(CustomResourceManager));

            return new DiagnosticDescriptor(
                "Id",
                localizableTitle,
                localizableMessage,
                "Category",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: localizableDescription);
        }

        private static DiagnosticDescriptor GetDescriptorWithCustomLocalizableStringsThatThrow()
        {
            // Test localizable title that throws.
            var localizableTitle = new ThrowingLocalizableString();
            var localizableMessage = new ThrowingLocalizableString();
            var localizableDescription = new ThrowingLocalizableString();

            return new DiagnosticDescriptor(
                "Id",
                localizableTitle,
                localizableMessage,
                "Category",
                DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                description: localizableDescription);
        }

        private class MyAnalyzer : DiagnosticAnalyzer
        {
            private readonly DiagnosticDescriptor _descriptor;

            public MyAnalyzer(DiagnosticDescriptor descriptor)
            {
                _descriptor = descriptor;
            }

            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
            {
                get
                {
                    return ImmutableArray.Create(_descriptor);
                }
            }

            public override void Initialize(AnalysisContext context)
            {
            }
        }

        private class ThrowingLocalizableString : LocalizableString
        {
            protected override bool AreEqual(object other)
            {
                throw new NotImplementedException();
            }

            protected override int GetHash()
            {
                throw new NotImplementedException();
            }

            protected override string GetText(IFormatProvider formatProvider)
            {
                throw new NotImplementedException();
            }
        }
    }
}
