﻿// 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.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.VisualStudio.LanguageServices.Options;
using Microsoft.VisualStudio.LanguageServices.UnitTests;
using Microsoft.VisualStudio.Settings;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests;

[UseExportProvider]
public class VisualStudioSettingsOptionPersisterTests
{
    private class MockSettingsSubset : ISettingsSubset
    {
        public event PropertyChangedAsyncEventHandler? SettingChangedAsync;

        public void TriggerSettingChanged(string storageName)
            => SettingChangedAsync?.Invoke(this, new PropertyChangedEventArgs(storageName));
    }

    private class MockSettingsManager : ISettingsManager
    {
        public Func<string, Type, (GetValueResult, object?)>? GetValueImpl;
        public Action<string, object?>? SetValueImpl;

        public readonly MockSettingsSubset Subset = new();

        public ISettingsSubset GetSubset(string namePattern)
            => Subset;

        public GetValueResult TryGetValue<T>(string name, out T value)
        {
            var (result, objValue) = GetValueImpl!(name, typeof(T));
            value = (result == GetValueResult.Success) ? (T)objValue! : default!;
            return result;
        }

        public Task SetValueAsync(string name, object? value, bool isMachineLocal)
        {
            SetValueImpl?.Invoke(name, value);
            return Task.CompletedTask;
        }

        public ISettingsList GetOrCreateList(string name, bool isMachineLocal)
            => throw new NotImplementedException();

        public T GetValueOrDefault<T>(string name, T defaultValue = default!)
            => throw new NotImplementedException();

        public string[] NamesStartingWith(string prefix)
            => throw new NotImplementedException();

        public void SetOnlineStore(IAsyncStringStorage store)
            => throw new NotImplementedException();

        public void SetSharedStore(IAsyncStringStorage store)
            => throw new NotImplementedException();
    }

    public enum E
    {
        A,
        B
    }

    private static readonly ImmutableDictionary<string, Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>> s_noFallbacks =
        ImmutableDictionary<string, Lazy<IVisualStudioStorageReadFallback, OptionNameMetadata>>.Empty;

    private static readonly NamingStylePreferences s_nonDefaultNamingStylePreferences = OptionsTestHelpers.GetNonDefaultNamingStylePreference();

    private static (object? value, object? storageValue) GetSomeOptionValue(Type optionType)
        => optionType == typeof(bool) ? (true, true) :
           optionType == typeof(string) ? ("str", "str") :
           optionType == typeof(int) ? (1, 1) :
           optionType == typeof(long) ? (1L, 1L) :
           optionType == typeof(bool?) ? (true, true) :
           optionType == typeof(int?) ? (1, 1) :
           optionType == typeof(long?) ? (1L, 1L) :
           optionType == typeof(E) ? (E.A, (int)E.A) :
           optionType == typeof(E?) ? (E.A, (int)E.A) :
           optionType == typeof(NamingStylePreferences) ? (s_nonDefaultNamingStylePreferences, s_nonDefaultNamingStylePreferences.CreateXElement().ToString()) :
           optionType == typeof(CodeStyleOption2<bool>) ? (new CodeStyleOption2<bool>(false, NotificationOption2.Warning), new CodeStyleOption2<bool>(false, NotificationOption2.Warning).ToXElement().ToString()) :
           optionType == typeof(ImmutableArray<bool>) ? (ImmutableArray.Create(true, false), new[] { true, false }) :
           optionType == typeof(ImmutableArray<int>) ? (ImmutableArray.Create(0, 1), new[] { 0, 1 }) :
           optionType == typeof(ImmutableArray<long>) ? (ImmutableArray.Create(0L, 1L), new[] { 0L, 1L }) :
           optionType == typeof(ImmutableArray<string>) ? (ImmutableArray.Create("a", "b"), new[] { "a", "b" }) :
           throw ExceptionUtilities.UnexpectedValue(optionType);

    private static Type GetStorageType(Type optionType)
        => optionType.IsEnum ? typeof(int) :
           (Nullable.GetUnderlyingType(optionType)?.IsEnum == true) ? typeof(int?) :
           optionType == typeof(NamingStylePreferences) ? typeof(string) :
           typeof(ICodeStyleOption2).IsAssignableFrom(optionType) ? typeof(string) :
           optionType == typeof(ImmutableArray<string>) ? typeof(string[]) :
           optionType == typeof(ImmutableArray<bool>) ? typeof(bool[]) :
           optionType == typeof(ImmutableArray<int>) ? typeof(int[]) :
           optionType == typeof(ImmutableArray<long>) ? typeof(long[]) :
           optionType;

    private static bool IsDefaultImmutableArray(object array)
        => (bool)array.GetType().GetMethod("get_IsDefault").Invoke(array, Array.Empty<object>())!;

    [Fact]
    public void SettingsChangeEvent()
    {
        var exportProvider = VisualStudioTestCompositions.LanguageServices.ExportProviderFactory.CreateExportProvider();
        var fallbacks = exportProvider.GetExports<IVisualStudioStorageReadFallback, OptionNameMetadata>().ToImmutableDictionary(item => item.Metadata.ConfigName, item => item);

        var refreshedOptions = new List<(OptionKey2, object?)>();
        var settingsManager = new MockSettingsManager();
        var persister = new VisualStudioSettingsOptionPersister((optionKey, newValue) => refreshedOptions.Add((optionKey, newValue)), fallbacks, settingsManager);
        var optionKey = new OptionKey2(CSharpFormattingOptions2.NewLineBeforeOpenBrace);

        // one flag is set:

        settingsManager.GetValueImpl = (name, type) => name switch
        {
            "TextEditor.CSharp.Specific.NewLinesForBracesInAnonymousTypes" => (GetValueResult.Success, false),
            _ => (GetValueResult.Missing, null),
        };

        Assert.True(persister.TryFetch(optionKey, "TextEditor.CSharp.Specific.csharp_new_line_before_open_brace", out var value));
        Assert.Equal(value, CSharpFormattingOptions2.NewLineBeforeOpenBrace.DefaultValue & ~NewLineBeforeOpenBracePlacement.AnonymousTypes);

        // Settings manager receives an option update (e.g. roaming options have been retrieved from online storage) and 
        // triggers option change event.

        settingsManager.GetValueImpl = (name, type) => name switch
        {
            "TextEditor.CSharp.Specific.NewLinesForBracesInMethods" => (GetValueResult.Success, false),
            _ => (GetValueResult.Missing, null),
        };

        settingsManager.Subset.TriggerSettingChanged("TextEditor.CSharp.Specific.NewLinesForBracesInMethods");

        Assert.Equal((optionKey, CSharpFormattingOptions2.NewLineBeforeOpenBrace.DefaultValue & ~NewLineBeforeOpenBracePlacement.Methods), refreshedOptions.Single());
        refreshedOptions.Clear();

        // Settings manager receives another option update -- the primary option is set now

        settingsManager.GetValueImpl = (name, type) => name switch
        {
            "TextEditor.CSharp.Specific.csharp_new_line_before_open_brace" => (GetValueResult.Success, NewLineBeforeOpenBracePlacement.Accessors | NewLineBeforeOpenBracePlacement.LambdaExpressionBody),
            "TextEditor.CSharp.Specific.NewLinesForBracesInAnonymousTypes" => (GetValueResult.Success, false),
            "TextEditor.CSharp.Specific.NewLinesForBracesInMethods" => (GetValueResult.Success, true),
            _ => (GetValueResult.Missing, null),
        };

        settingsManager.Subset.TriggerSettingChanged("TextEditor.CSharp.Specific.csharp_new_line_before_open_brace");

        Assert.Equal((optionKey, NewLineBeforeOpenBracePlacement.Accessors | NewLineBeforeOpenBracePlacement.LambdaExpressionBody), refreshedOptions.Single());
        refreshedOptions.Clear();
    }

    [Theory]
    [CombinatorialData]
    public void SettingsManagerReadOptionValue_Success(
        [CombinatorialValues(
            typeof(bool),
            typeof(string),
            typeof(int),
            typeof(long),
            typeof(bool?),
            typeof(int?),
            typeof(long?),
            typeof(E),
            typeof(E?),
            typeof(CodeStyleOption2<bool>),
            typeof(NamingStylePreferences),
            typeof(ImmutableArray<bool>),
            typeof(ImmutableArray<int>),
            typeof(ImmutableArray<long>),
            typeof(ImmutableArray<string>))]
        Type optionType)
    {
        var (optionValue, storageValue) = GetSomeOptionValue(optionType);
        var defaultValue = OptionsTestHelpers.GetDifferentValue(optionType, optionValue);

        var mockManager = new MockSettingsManager()
        {
            GetValueImpl = (_, _) => (GetValueResult.Success, storageValue)
        };

        var result = VisualStudioSettingsOptionPersister.TryReadOptionValue(mockManager, "key", optionType, defaultValue);
        Assert.True(result.HasValue);
        Assert.Equal(optionValue, result.Value);
    }

    [Theory]
    [CombinatorialData]
    public void SettingsManagerReadOptionValue_Error(
        [CombinatorialValues(
            GetValueResult.Missing,
            GetValueResult.IncompatibleType,
            GetValueResult.ObsoleteFormat,
            GetValueResult.UnknownError,
            GetValueResult.Corrupt)]
        GetValueResult specializedTypeResult,
        [CombinatorialValues(
            typeof(bool),
            typeof(string),
            typeof(int),
            typeof(long),
            typeof(bool?),
            typeof(int?),
            typeof(long?),
            typeof(E),
            typeof(E?),
            typeof(CodeStyleOption2<bool>),
            typeof(NamingStylePreferences),
            typeof(ImmutableArray<bool>),
            typeof(ImmutableArray<int>),
            typeof(ImmutableArray<long>),
            typeof(ImmutableArray<string>))]
        Type optionType)
    {
        var (optionValue, storageValue) = GetSomeOptionValue(optionType);
        var storageType = GetStorageType(optionType);

        var mockManager = new MockSettingsManager()
        {
            GetValueImpl = (_, type) => (type == storageType ? specializedTypeResult : GetValueResult.Success, storageValue)
        };

        var result = VisualStudioSettingsOptionPersister.TryReadOptionValue(mockManager, "key", optionType, optionValue);

        // we should not fall back to object if the manager tells us bool value is missing or invalid in any way:
        Assert.False(result.HasValue);
    }

    [Theory]
    [InlineData(typeof(ImmutableArray<bool>))]
    [InlineData(typeof(ImmutableArray<int>))]
    [InlineData(typeof(ImmutableArray<long>))]
    [InlineData(typeof(ImmutableArray<string>))]
    public void Roundtrip_DefaultImmutableArray(Type type)
    {
        var defaultArray = Activator.CreateInstance(type);
        var serializedValue = (object?)null;

        Optional<object?> newValue = default;

        var mockManager = new MockSettingsManager()
        {
            GetValueImpl = (_, _) => (GetValueResult.Success, serializedValue),
            SetValueImpl = (name, value) => newValue = value
        };

        // read
        var result = VisualStudioSettingsOptionPersister.TryReadOptionValue(mockManager, "key", type, defaultValue: null);
        Assert.True(result.HasValue);
        Assert.True(IsDefaultImmutableArray(result.Value!));

        // write
        var persister = new VisualStudioSettingsOptionPersister((_, _) => { }, s_noFallbacks, mockManager);
        persister.PersistAsync("key", defaultArray).Wait();

        Assert.True(newValue.HasValue);
        Assert.Equal(serializedValue, newValue.Value);
    }

    public static IEnumerable<object?[]> GetRoundtripTestCases()
    {
        var value1 = new CodeStyleOption2<int>(1, NotificationOption2.Warning);
        yield return new object?[] { value1, value1.ToXElement().ToString(), new CodeStyleOption2<int>(0, NotificationOption2.None), typeof(CodeStyleOption2<int>) };

        var value2 = s_nonDefaultNamingStylePreferences;
        yield return new object?[] { value2, value2.CreateXElement().ToString(), NamingStylePreferences.Empty, typeof(NamingStylePreferences) };

        yield return new object?[] { E.B, (int)E.B, E.B, typeof(E) };
        yield return new object?[] { null, null, E.B, typeof(E?) };
        yield return new object?[] { (E?)E.B, (int?)E.B, E.B, typeof(E?) };
    }

    [Theory]
    [MemberData(nameof(GetRoundtripTestCases))]
    public void Roundtrip(object? value, object? serializedValue, object defaultValue, Type type)
    {
        Optional<object?> newValue = default;

        var mockManager = new MockSettingsManager()
        {
            GetValueImpl = (_, _) => (GetValueResult.Success, serializedValue),
            SetValueImpl = (name, value) => newValue = value
        };

        // read
        var result = VisualStudioSettingsOptionPersister.TryReadOptionValue(mockManager, "key", type, defaultValue);
        Assert.True(result.HasValue);
        Assert.Equal(value, result.Value);

        // write
        var persister = new VisualStudioSettingsOptionPersister((_, _) => { }, s_noFallbacks, mockManager);
        persister.PersistAsync("key", value).Wait();

        Assert.True(newValue.HasValue);
        Assert.Equal(serializedValue, newValue.Value);
    }
}
