﻿// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ChangeSignature;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Test.Utilities.ChangeSignature;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.ChangeSignature
{
    public abstract class AbstractChangeSignatureTests : AbstractCodeActionTest
    {
        protected override ParseOptions GetScriptOptions()
            => throw new NotSupportedException();

        protected override TestComposition GetComposition()
            => base.GetComposition().AddParts(typeof(TestChangeSignatureOptionsService));

        internal async Task TestChangeSignatureViaCodeActionAsync(
            string markup,
            bool expectedCodeAction = true,
            int[] updatedSignature = null,
            string expectedCode = null,
            int index = 0)
            => await TestChangeSignatureViaCodeActionAsync(
                markup, updatedSignature?.Select(i => new AddedParameterOrExistingIndex(i)).ToArray(),
                expectedCodeAction, expectedCode, index).ConfigureAwait(false);

        internal async Task TestChangeSignatureViaCodeActionAsync(
            string markup,
            AddedParameterOrExistingIndex[] updatedSignature,
            bool expectedCodeAction = true,
            string expectedCode = null,
            int index = 0)
        {
            if (expectedCodeAction)
            {
                var testOptions = new TestParameters();

                using var workspace = CreateWorkspaceFromOptions(markup, testOptions);
                var optionsService = (TestChangeSignatureOptionsService)workspace.Services.GetRequiredService<IChangeSignatureOptionsService>();
                optionsService.UpdatedSignature = updatedSignature;

                var refactoring = await GetCodeRefactoringAsync(workspace, testOptions);
                await TestActionAsync(workspace, expectedCode, refactoring.CodeActions[index].action,
                    conflictSpans: ImmutableArray<TextSpan>.Empty,
                    renameSpans: ImmutableArray<TextSpan>.Empty,
                    warningSpans: ImmutableArray<TextSpan>.Empty,
                    navigationSpans: ImmutableArray<TextSpan>.Empty,
                    parameters: null);
            }
            else
            {
                await TestMissingAsync(markup);
            }
        }

        internal static async Task TestChangeSignatureViaCommandAsync(
            string languageName,
            string markup,
            bool expectedSuccess = true,
            int[] updatedSignature = null,
            string expectedUpdatedInvocationDocumentCode = null,
            ChangeSignatureFailureKind? expectedFailureReason = null,
            int? totalParameters = null,
            bool verifyNoDiagnostics = false,
            ParseOptions parseOptions = null,
            OptionsCollection options = null,
            int expectedSelectedIndex = -1)
            => await TestChangeSignatureViaCommandAsync(languageName, markup,
                updatedSignature?.Select(i => new AddedParameterOrExistingIndex(i)).ToArray(),
                expectedSuccess, expectedUpdatedInvocationDocumentCode,
                expectedFailureReason,
                totalParameters,
                verifyNoDiagnostics,
                parseOptions,
                options,
                expectedSelectedIndex);

        internal static async Task TestChangeSignatureViaCommandAsync(
            string languageName,
            string markup,
            AddedParameterOrExistingIndex[] updatedSignature,
            bool expectedSuccess = true,
            string expectedUpdatedInvocationDocumentCode = null,
            ChangeSignatureFailureKind? expectedFailureReason = null,
            int? totalParameters = null,
            bool verifyNoDiagnostics = false,
            ParseOptions parseOptions = null,
            OptionsCollection options = null,
            int expectedSelectedIndex = -1)
        {
            using (var testState = ChangeSignatureTestState.Create(markup, languageName, parseOptions, options))
            {
                testState.TestChangeSignatureOptionsService.UpdatedSignature = updatedSignature;
                var result = await testState.ChangeSignatureAsync().ConfigureAwait(false);

                if (expectedSuccess)
                {
                    Assert.True(result.Succeeded);
                    Assert.Null(result.ChangeSignatureFailureKind);
                }
                else
                {
                    Assert.False(result.Succeeded);

                    if (expectedFailureReason != null)
                    {
                        Assert.Equal(expectedFailureReason, result.ChangeSignatureFailureKind);
                    }
                }

                // Allow testing of invocation document regardless of success/failure
                if (expectedUpdatedInvocationDocumentCode != null)
                {
                    var updatedInvocationDocument = result.UpdatedSolution.GetDocument(testState.InvocationDocument.Id);
                    var updatedCode = (await updatedInvocationDocument.GetTextAsync()).ToString();
                    Assert.Equal(expectedUpdatedInvocationDocumentCode, updatedCode);
                }

                if (verifyNoDiagnostics)
                {
                    var diagnostics = (await testState.InvocationDocument.GetSemanticModelAsync()).GetDiagnostics();

                    if (diagnostics.Length > 0)
                    {
                        Assert.True(false, CreateDiagnosticsString(diagnostics, updatedSignature, testState.InvocationDocument, totalParameters, (await testState.InvocationDocument.GetTextAsync()).ToString()));
                    }
                }

                if (expectedSelectedIndex != -1)
                {
                    var parameterConfiguration = await testState.GetParameterConfigurationAsync();
                    Assert.Equal(expectedSelectedIndex, parameterConfiguration.SelectedIndex);
                }
            }
        }

        private static string CreateDiagnosticsString(ImmutableArray<Diagnostic> diagnostics, AddedParameterOrExistingIndex[] permutation, Document document, int? totalParameters, string fileContents)
        {
            if (diagnostics.Length == 0)
            {
                return string.Empty;
            }

            return string.Format("{0} diagnostic(s) introduced in signature configuration \"{1}\":\r\n{2}\r\n{3}",
                diagnostics.Length,
                GetSignatureDescriptionString(document, permutation, totalParameters),
                string.Join("\r\n", diagnostics.Select(d => d.GetMessage())),
                fileContents);
        }

        private static string GetSignatureDescriptionString(Document document, AddedParameterOrExistingIndex[] signature, int? totalParameters)
        {
            var existingParametersKept = signature.Where(p => p.IsExisting).Select(p => p.OldIndex).ToArray();
            var removeDescription = string.Empty;
            if (totalParameters.HasValue)
            {
                var removed = new List<int>();
                for (var i = 0; i < totalParameters; i++)
                {
                    if (!existingParametersKept.Contains(i))
                    {
                        removed.Add(i);
                    }
                }

                removeDescription = removed.Any() ? string.Format(", Removed: {{{0}}}", string.Join(", ", removed)) : string.Empty;
            }

            var newParametersString = string.Join(",", signature.Where(p => !p.IsExisting).Select(p => p.GetAddedParameter(document)));
            var addDescription = !newParametersString.IsEmpty() ? string.Format(", Added {{{0}}}", newParametersString) : string.Empty;

            return string.Format("Parameters: <{0}>{1}{2}", string.Join(", ", signature.Select(item => item.ToString())), removeDescription, addDescription);
        }

        /// <summary>
        /// Tests all possible changed signatures to ensure no diagnostics are introduced. Given a
        /// signature 
        ///     Tr M(this T0 t0, T1 t1, ... Tm tm, D1 d1 = v1, ..., Dn dn = vn, params U u)
        /// with s this parameters (0 or 1), m >= 0 simple parameters, n >= 0 parameters with
        /// default values, and p params parameters (0 or 1), there are 
        /// Π s∈{m, n} (Σ k=0 to s (sCk * (s-k)!)) * 2^p - 1 changed signatures to consider.
        /// </summary>
        /// <param name="signaturePartCounts">A four element array containing [s, m, n, p] as 
        /// described above.</param>
        public static IEnumerable<object[]> GetAllSignatureSpecificationsForTheory(int[] signaturePartCounts)
        {
            Assert.Equal(4, signaturePartCounts.Length);
            Assert.True(signaturePartCounts[0] is 0 or 1);
            Assert.True(signaturePartCounts[3] is 0 or 1);

            var totalParameters = signaturePartCounts[0] + signaturePartCounts[1] + signaturePartCounts[2] + signaturePartCounts[3];

            foreach (var signature in GetAllSignatureSpecifications(signaturePartCounts))
            {
                yield return new object[] { totalParameters, signature };
            }
        }

        private static IEnumerable<int[]> GetAllSignatureSpecifications(int[] signaturePartCounts)
        {
            var regularParameterStartIndex = signaturePartCounts[0];
            var defaultValueParameterStartIndex = signaturePartCounts[0] + signaturePartCounts[1];
            var paramParameterIndex = signaturePartCounts[0] + signaturePartCounts[1] + signaturePartCounts[2];

            var regularParameterArrangements = GetPermutedSubsets(regularParameterStartIndex, signaturePartCounts[1]);
            var defaultValueParameterArrangements = GetPermutedSubsets(defaultValueParameterStartIndex, signaturePartCounts[2]);

            var startArray = signaturePartCounts[0] == 0 ? Array.Empty<int>() : new[] { 0 };

            foreach (var regularParameterPart in regularParameterArrangements)
            {
                foreach (var defaultValueParameterPart in defaultValueParameterArrangements)
                {
                    var p1 = startArray.Concat(regularParameterPart).Concat(defaultValueParameterPart);
                    yield return p1.ToArray();

                    if (signaturePartCounts[3] == 1)
                    {
                        yield return p1.Concat(new[] { paramParameterIndex }).ToArray();
                    }
                }
            }
        }

        private static IEnumerable<IEnumerable<int>> GetPermutedSubsets(int startIndex, int count)
        {
            foreach (var subset in GetSubsets(Enumerable.Range(startIndex, count)))
            {
                foreach (var permutation in GetPermutations(subset))
                {
                    yield return permutation;
                }
            }
        }

        private static IEnumerable<IEnumerable<int>> GetPermutations(IEnumerable<int> list)
        {
            if (!list.Any())
            {
                yield return SpecializedCollections.EmptyEnumerable<int>();
                yield break;
            }

            var index = 0;
            foreach (var element in list)
            {
                var permutationsWithoutElement = GetPermutations(GetListWithoutElementAtIndex(list, index));
                foreach (var perm in permutationsWithoutElement)
                {
                    yield return perm.Concat(element);
                }

                index++;
            }
        }

        private static IEnumerable<int> GetListWithoutElementAtIndex(IEnumerable<int> list, int skippedIndex)
        {
            var index = 0;
            foreach (var x in list)
            {
                if (index != skippedIndex)
                {
                    yield return x;
                }

                index++;
            }
        }

        private static IEnumerable<IEnumerable<int>> GetSubsets(IEnumerable<int> list)
        {
            if (!list.Any())
            {
                return SpecializedCollections.SingletonEnumerable(SpecializedCollections.EmptyEnumerable<int>());
            }

            var firstElement = list.Take(1);

            var withoutFirstElement = GetSubsets(list.Skip(1));
            var withFirstElement = withoutFirstElement.Select(without => firstElement.Concat(without));

            return withoutFirstElement.Concat(withFirstElement);
        }
    }
}
